#!/usr/bin/env python3

from xasyqtui.window1 import Ui_MainWindow

import PyQt5.QtWidgets as Qw
import PyQt5.QtGui as Qg
import PyQt5.QtCore as Qc
from xasyversion.version import VERSION as xasyVersion

import numpy as np
import os
import json
import io
import pathlib
import webbrowser
import subprocess
import tempfile
import datetime
import string
import atexit
import pickle

import xasyUtils as xu
import xasy2asy as x2a
import xasyFile as xf
import xasyOptions as xo
import UndoRedoStack as Urs
import xasyArgs as xa
import xasyBezierInterface as xbi
from xasyTransform import xasyTransform as xT
import xasyStrings as xs

import PrimitiveShape
import InplaceAddObj
import ContextWindow

import CustMatTransform
import SetCustomAnchor
import GuidesManager


class ActionChanges:
   pass


# State Invariance: When ActionChanges is at the top, all state of the program & file
# is exactly like what it was the event right after that ActionChanges was created.

class TransformationChanges(ActionChanges):
   def __init__(self, objIndex, transformation, isLocal=False):
       self.objIndex = objIndex
       self.transformation = transformation
       self.isLocal = isLocal

class ObjCreationChanges(ActionChanges):
   def __init__(self, obj):
       self.object = obj

class HardDeletionChanges(ActionChanges):
   def __init__(self, obj, pos):
       self.item = obj
       self.objIndex = pos

class SoftDeletionChanges(ActionChanges):
   def __init__(self, obj, keyPos):
       self.item = obj
       self.keyMap = keyPos

class EditBezierChanges(ActionChanges):
   def __init__(self, obj, pos, oldPath, newPath):
       self.item = obj
       self.objIndex = pos
       self.oldPath = oldPath
       self.newPath = newPath

class AnchorMode:
   center = 0
   origin = 1
   topLeft = 2
   topRight = 3
   bottomRight = 4
   bottomLeft = 5
   customAnchor = 6


class GridMode:
   cartesian = 0
   polar = 1


class SelectionMode:
   select = 0
   pan = 1
   translate = 2
   rotate = 3
   scale = 4
   delete = 5
   setAnchor = 6
   selectEdit = 7
   openPoly = 8
   closedPoly = 9
   openCurve = 10
   closedCurve = 11
   addPoly = 12
   addCircle = 13
   addLabel = 14
   addFreehand = 15

class AddObjectMode:
   Circle = 0
   Arc = 1
   Polygon = 2

class MainWindow1(Qw.QMainWindow):
   defaultFrameStyle = """
   QFrame{{
       padding: 4.0;
       border-radius: 3.0;
       background: rgb({0}, {1}, {2})
   }}
   """

   def __init__(self):
       self.testingActions = []
       super().__init__()
       self.ui = Ui_MainWindow()
       global devicePixelRatio
       devicePixelRatio=self.devicePixelRatio()
       self.ui.setupUi(self)
       self.ui.menubar.setNativeMenuBar(False)
       self.setWindowIcon(Qg.QIcon("../asy.ico"))

       self.settings = xo.BasicConfigs.defaultOpt
       self.keyMaps = xo.BasicConfigs.keymaps
       self.openRecent = xo.BasicConfigs.openRecent

       self.raw_args = Qc.QCoreApplication.arguments()
       self.args = xa.parseArgs(self.raw_args)

       self.strings = xs.xasyString(self.args.language)
       self.asy2psmap = x2a.yflip()

       if self.settings['asyBaseLocation'] is not None:
           os.environ['ASYMPTOTE_DIR'] = self.settings['asyBaseLocation']

       addrAsyArgsRaw: str = self.args.additionalAsyArgs or \
           self.settings.get('additionalAsyArgs', "")

       self.asyPath = self.args.asypath or self.settings.get('asyPath')
       self.asyEngine = x2a.AsymptoteEngine(
           self.asyPath,
           None if not addrAsyArgsRaw else addrAsyArgsRaw.split(',')
       )

       try:
           self.asyEngine.start()
       finally:
           atexit.register(self.asyEngine.cleanup)

       # For initialization purposes
       self.canvSize = Qc.QSize()
       self.fileName = None
       self.asyFileName = None
       self.currDir = None
       self.mainCanvas = None
       self.dpi = 300
       self.canvasPixmap = None
       self.tx=0
       self.ty=0

       # Actions
       # <editor-fold> Connecting Actions
       self.ui.txtLineWidth.setValidator(Qg.QDoubleValidator())

       self.connectActions()
       self.connectButtons()

       self.ui.txtLineWidth.returnPressed.connect(self.btnTerminalCommandOnClick)
       # </editor-fold>

       # Base Transformations

       self.mainTransformation = Qg.QTransform()
       self.mainTransformation.scale(1, 1)
       self.localTransform = Qg.QTransform()
       self.screenTransformation = Qg.QTransform()
       self.panTranslation = Qg.QTransform()

       # Internal Settings
       self.magnification = self.args.mag
       self.inMidTransformation = False
       self.addMode = None
       self.currentlySelectedObj = {'key': None, 'allSameKey': set(), 'selectedIndex': None, 'keyIndex': None}
       self.pendingSelectedObjList = []
       self.pendingSelectedObjIndex = -1

       self.savedMousePosition = None
       self.currentBoundingBox = None
       self.selectionDelta = None
       self.newTransform = None
       self.origBboxTransform = None
       self.deltaAngle = 0
       self.scaleFactor = 1
       self.panOffset = [0, 0]

       # Keyboard can focus outside of textboxes
       self.setFocusPolicy(Qc.Qt.StrongFocus)

       super().setMouseTracking(True)
       # setMouseTracking(True)

       self.undoRedoStack = Urs.actionStack()

       self.lockX = False
       self.lockY = False
       self.anchorMode = AnchorMode.center
       self.currentAnchor = Qc.QPointF(0, 0)
       self.customAnchor = None
       self.useGlobalCoords = True
       self.drawAxes = True
       self.drawGrid = False
       self.gridSnap = False  # TODO: for now. turn it on later

       self.fileChanged = False

       self.terminalPythonMode = self.ui.btnTogglePython.isChecked()

       self.savedWindowMousePos = None

       self.finalPixmap = None
       self.postCanvasPixmap = None
       self.previewCurve = None
       self.mouseDown = False

       self.globalObjectCounter = 1

       self.fileItems = []
       self.drawObjects = []
       self.xasyDrawObj = {'drawDict': self.drawObjects}

       self.modeButtons = {
           self.ui.btnTranslate, self.ui.btnRotate, self.ui.btnScale, # self.ui.btnSelect,
           self.ui.btnPan, self.ui.btnDeleteMode, self.ui.btnAnchor,
           self.ui.btnSelectEdit, self.ui.btnOpenPoly, self.ui.btnClosedPoly,
           self.ui.btnOpenCurve, self.ui.btnClosedCurve, self.ui.btnAddPoly,
           self.ui.btnAddCircle, self.ui.btnAddLabel, self.ui.btnAddFreehand
                           }

       self.objButtons = {self.ui.btnCustTransform, self.ui.actionTransform, self.ui.btnSendForwards,
                          self.ui.btnSendBackwards, self.ui.btnToggleVisible
                          }

       self.globalTransformOnlyButtons = (self.ui.comboAnchor, self.ui.btnAnchor)

       self.ui.txtTerminalPrompt.setFont(Qg.QFont(self.settings['terminalFont']))

       self.currAddOptionsWgt = None
       self.currAddOptions = {
           'options': self.settings,
           'inscribed': True,
           'sides': 3,
           'centermode': True,
           'fontSize': None,
           'asyengine': self.asyEngine,
           'fill': self.ui.btnFill.isChecked(),
           'closedPath': False,
           'useBezier': True,
           'magnification': self.magnification,
           'editBezierlockMode': xbi.Web.LockMode.angleLock,
           'autoRecompute': False
       }


       self.currentModeStack = [SelectionMode.translate]
       self.drawGridMode = GridMode.cartesian
       self.setAllInSetEnabled(self.objButtons, False)
       self._currentPen = x2a.asyPen()
       self.currentGuides = []
       self.selectAsGroup = self.settings['groupObjDefault']

       # commands switchboard
       self.commandsFunc = {
           'quit': self.btnCloseFileonClick,
           'undo': self.btnUndoOnClick,
           'redo': self.btnRedoOnClick,
           'manual': self.actionManual,
           'about': self.actionAbout,
           'loadFile': self.btnLoadFileonClick,
           'save': self.actionSave,
           'saveAs': self.actionSaveAs,
           'transform': self.btnCustTransformOnClick,
           'commandPalette': self.enterCustomCommand,
           'clearGuide': self.clearGuides,
           'finalizeAddObj': self.finalizeAddObj,
           'finalizeCurve': self.finalizeCurve,
           'finalizeCurveClosed': self.finalizeCurveClosed,
           'setMag': self.setMagPrompt,
           'deleteObject': self.btnSelectiveDeleteOnClick,
           'anchorMode': self.switchToAnchorMode,
           'moveUp': lambda: self.translate(0, -1),
           'moveDown': lambda: self.translate(0, 1),
           'moveLeft': lambda: self.translate(-1, 0),
           'moveRight': lambda: self.translate(1, 0),

           'scrollLeft': lambda: self.arrowButtons(-1, 0, True),
           'scrollRight': lambda: self.arrowButtons(1, 0, True),
           'scrollUp': lambda: self.arrowButtons(0, 1, True),
           'scrollDown': lambda: self.arrowButtons(0, -1, True),

           'zoomIn': lambda: self.arrowButtons(0, 1, False, True),
           'zoomOut': lambda: self.arrowButtons(0, -1, False, True),

           'open': self.btnLoadFileonClick,
           'save': self.actionSave,
           'export': self.btnExportAsymptoteOnClick,

           'copy': self.copyItem,
           'paste': self.pasteItem
       }

       self.hiddenKeys = set()

       # Coordinates Label

       self.coordLabel = Qw.QLabel(self.ui.statusbar)
       self.ui.statusbar.addPermanentWidget(self.coordLabel)

       # Settings Initialization
       # from xasyoptions config file
       self.loadKeyMaps()
       self.setupXasyOptions()
       self.populateOpenRecent()

       self.colorDialog = Qw.QColorDialog(x2a.asyPen.convertToQColor(self._currentPen.color), self)
       self.initPenInterface()

   def arrowButtons(self, x:int , y:int, shift: bool=False, ctrl: bool=False):
       "x, y indicates update button orientation on the cartesian plane."
       if not (shift or ctrl):
           self.changeSelection(y)
       elif not (shift and ctrl):
           self.mouseWheel(30*x, 30*y)
       self.quickUpdate()

   def translate(self, x:int , y:int):
       "x, y indicates update button orientation on the cartesian plane."
       if self.lockX:
           x = 0
       if self.lockY:
           y = 0
       self.tx += x
       self.ty += y
       self.newTransform=Qg.QTransform.fromTranslate(self.tx,self.ty)
       self.quickUpdate()

   def cleanup(self):
       self.asyengine.cleanup()

   def getScrsTransform(self):
       # pipeline:
       # assuming origin <==> top left
       # (Pan) * (Translate) * (Flip the images) * (Zoom) * (Obj transform) * (Base Information)

       # pipeline --> let x, y be the postscript point
       # p = (mx + cx + panoffset, -ny + cy + panoffset)
       factor=0.5/devicePixelRatio;
       cx, cy = self.canvSize.width()*factor, self.canvSize.height()*factor

       newTransf = Qg.QTransform()
       newTransf.translate(*self.panOffset)
       newTransf.translate(cx, cy)
       newTransf.scale(1, 1)
       newTransf.scale(self.magnification, self.magnification)

       return newTransf

   def finalizeCurve(self):
       if self.addMode is not None:
           if self.addMode.active and isinstance(self.addMode, InplaceAddObj.AddBezierShape):
               self.addMode.forceFinalize()
               self.fileChanged = True

   def finalizeCurveClosed(self):
       if self.addMode is not None:
           if self.addMode.active and isinstance(self.addMode, InplaceAddObj.AddBezierShape):
               self.addMode.finalizeClosure()
               self.fileChanged = True

   def getAllBoundingBox(self) -> Qc.QRectF:
       newRect = Qc.QRectF()
       for majitem in self.drawObjects:
           for minitem in majitem:
               newRect = newRect.united(minitem.boundingBox)
       return newRect

   def finalizeAddObj(self):
       if self.addMode is not None:
           if self.addMode.active:
               self.addMode.forceFinalize()
               self.fileChanged = True

   def openAndReloadSettings(self):
       settingsFile = self.settings.settingsFileLocation()
       subprocess.run(args=self.getExternalEditor(asypath=settingsFile))
       self.settings.load()
       self.quickUpdate()

   def openAndReloadKeymaps(self):
       keymapsFile = self.keyMaps.settingsFileLocation()
       subprocess.run(args=self.getExternalEditor(asypath=keymapsFile))
       self.settings.load()
       self.quickUpdate()

   def setMagPrompt(self):
       commandText, result = Qw.QInputDialog.getText(self, '', 'Enter magnification:')
       if result:
           self.magnification = float(commandText)
           self.currAddOptions['magnification'] = self.magnification
           self.quickUpdate()

   def setTextPrompt(self):
       commandText, result = Qw.QInputDialog.getText(self, '', 'Enter new text:')
       if result:
           return commandText

   def btnTogglePythonOnClick(self, checked):
       self.terminalPythonMode = checked

   def internationalize(self):
       self.ui.btnRotate.setToolTip(self.strings.rotate)

   def handleArguments(self):
       if self.args.filename is not None:
           if os.path.exists(self.args.filename):
               self.actionOpen(os.path.abspath(self.args.filename))
           else:
               self.loadFile(self.args.filename)
       else:
           self.initializeEmptyFile()

       if self.args.language != 'en':
           self.internationalize()

   def initPenInterface(self):
       self.ui.txtLineWidth.setText(str(self._currentPen.width))
       self.updateFrameDispColor()

   def updateFrameDispColor(self):
       r, g, b = [int(x * 255) for x in self._currentPen.color]
       self.ui.frameCurrColor.setStyleSheet(MainWindow1.defaultFrameStyle.format(r, g, b))

   def initDebug(self):
       debugFunc = {
       }
       self.commandsFunc = {**self.commandsFunc, **debugFunc}

   def dbgRecomputeCtrl(self):
       if isinstance(self.addMode, xbi.InteractiveBezierEditor):
           self.addMode.recalculateCtrls()
           self.quickUpdate()

   def objectUpdated(self):
       self.removeAddMode()
       self.clearSelection()
       self.asyfyCanvas()

   def connectActions(self):
       self.ui.actionQuit.triggered.connect(lambda: self.execCustomCommand('quit'))
       self.ui.actionUndo.triggered.connect(lambda: self.execCustomCommand('undo'))
       self.ui.actionRedo.triggered.connect(lambda: self.execCustomCommand('redo'))
       self.ui.actionTransform.triggered.connect(lambda: self.execCustomCommand('transform'))

       self.ui.actionNewFile.triggered.connect(self.actionNewFile)
       self.ui.actionOpen.triggered.connect(self.actionOpen)
       self.ui.actionClearRecent.triggered.connect(self.actionClearRecent)
       self.ui.actionSave.triggered.connect(self.actionSave)
       self.ui.actionSaveAs.triggered.connect(self.actionSaveAs)
       self.ui.actionManual.triggered.connect(self.actionManual)
       self.ui.actionAbout.triggered.connect(self.actionAbout)
       self.ui.actionSettings.triggered.connect(self.openAndReloadSettings)
       self.ui.actionKeymaps.triggered.connect(self.openAndReloadKeymaps)
       self.ui.actionEnterCommand.triggered.connect(self.enterCustomCommand)
       self.ui.actionExportAsymptote.triggered.connect(self.btnExportAsymptoteOnClick)
       self.ui.actionExportToAsy.triggered.connect(self.btnExportToAsyOnClick)

   def setupXasyOptions(self):
       if self.settings['debugMode']:
           self.initDebug()
       newColor = Qg.QColor(self.settings['defaultPenColor'])
       newWidth = self.settings['defaultPenWidth']

       self._currentPen.setColorFromQColor(newColor)
       self._currentPen.setWidth(newWidth)

   def connectButtons(self):
       # Button initialization
       self.ui.btnUndo.clicked.connect(self.btnUndoOnClick)
       self.ui.btnRedo.clicked.connect(self.btnRedoOnClick)
       self.ui.btnLoadFile.clicked.connect(self.btnLoadFileonClick)
       self.ui.btnSave.clicked.connect(self.btnSaveonClick)
       self.ui.btnQuickScreenshot.clicked.connect(self.btnQuickScreenshotOnClick)

       # self.ui.btnExportAsy.clicked.connect(self.btnExportAsymptoteOnClick)

       self.ui.btnDrawAxes.clicked.connect(self.btnDrawAxesOnClick)
#        self.ui.btnAsyfy.clicked.connect(lambda: self.asyfyCanvas(True))
       self.ui.btnSetZoom.clicked.connect(self.setMagPrompt)
       self.ui.btnResetPan.clicked.connect(self.resetPan)
       self.ui.btnPanCenter.clicked.connect(self.btnPanCenterOnClick)

       self.ui.btnTranslate.clicked.connect(self.btnTranslateonClick)
       self.ui.btnRotate.clicked.connect(self.btnRotateOnClick)
       self.ui.btnScale.clicked.connect(self.btnScaleOnClick)
       # self.ui.btnSelect.clicked.connect(self.btnSelectOnClick)
       self.ui.btnPan.clicked.connect(self.btnPanOnClick)

       # self.ui.btnDebug.clicked.connect(self.pauseBtnOnClick)
       self.ui.btnAlignX.clicked.connect(self.btnAlignXOnClick)
       self.ui.btnAlignY.clicked.connect(self.btnAlignYOnClick)
       self.ui.comboAnchor.currentIndexChanged.connect(self.handleAnchorComboIndex)
       self.ui.btnCustTransform.clicked.connect(self.btnCustTransformOnClick)
       self.ui.btnViewCode.clicked.connect(self.btnLoadEditorOnClick)

       self.ui.btnAnchor.clicked.connect(self.btnAnchorModeOnClick)

       self.ui.btnSelectColor.clicked.connect(self.btnColorSelectOnClick)
       self.ui.txtLineWidth.textEdited.connect(self.txtLineWidthEdited)

       # self.ui.btnCreateCurve.clicked.connect(self.btnCreateCurveOnClick)
       self.ui.btnDrawGrid.clicked.connect(self.btnDrawGridOnClick)

       self.ui.btnAddCircle.clicked.connect(self.btnAddCircleOnClick)
       self.ui.btnAddPoly.clicked.connect(self.btnAddPolyOnClick)
       self.ui.btnAddLabel.clicked.connect(self.btnAddLabelOnClick)
       self.ui.btnAddFreehand.clicked.connect(self.btnAddFreehandOnClick)
       # self.ui.btnAddBezierInplace.clicked.connect(self.btnAddBezierInplaceOnClick)
       self.ui.btnClosedCurve.clicked.connect(self.btnAddClosedCurveOnClick)
       self.ui.btnOpenCurve.clicked.connect(self.btnAddOpenCurveOnClick)
       self.ui.btnClosedPoly.clicked.connect(self.btnAddClosedLineOnClick)
       self.ui.btnOpenPoly.clicked.connect(self.btnAddOpenLineOnClick)

       self.ui.btnFill.clicked.connect(self.btnFillOnClick)

       self.ui.btnSendBackwards.clicked.connect(self.btnSendBackwardsOnClick)
       self.ui.btnSendForwards.clicked.connect(self.btnSendForwardsOnClick)
       # self.ui.btnDelete.clicked.connect(self.btnSelectiveDeleteOnClick)
       self.ui.btnDeleteMode.clicked.connect(self.btnDeleteModeOnClick)
       # self.ui.btnSoftDelete.clicked.connect(self.btnSoftDeleteOnClick)
       self.ui.btnToggleVisible.clicked.connect(self.btnSetVisibilityOnClick)

       self.ui.btnEnterCommand.clicked.connect(self.btnTerminalCommandOnClick)
       self.ui.btnTogglePython.clicked.connect(self.btnTogglePythonOnClick)
       self.ui.btnSelectEdit.clicked.connect(self.btnSelectEditOnClick)

   def btnDeleteModeOnClick(self):
       if self.currentModeStack[-1] != SelectionMode.delete:
           self.currentModeStack = [SelectionMode.delete]
           self.ui.statusbar.showMessage('Delete mode')
           self.clearSelection()
           self.updateChecks()
       else:
           self.btnTranslateonClick()

   def btnTerminalCommandOnClick(self):
       if self.terminalPythonMode:
           exec(self.ui.txtTerminalPrompt.text())
           self.fileChanged = True
       else:
           pass
           # TODO: How to handle this case?
           # Like AutoCAD?
       self.ui.txtTerminalPrompt.clear()

   def btnFillOnClick(self, checked):
       self.currAddOptions['fill'] = checked
       self.ui.btnOpenCurve.setEnabled(not checked)
       self.ui.btnOpenPoly.setEnabled(not checked)

   def btnSelectEditOnClick(self):
       if self.currentModeStack[-1] != SelectionMode.selectEdit:
           self.currentModeStack = [SelectionMode.selectEdit]
           self.ui.statusbar.showMessage('Edit mode')
           self.clearSelection()
           self.updateChecks()
       else:
           self.btnTranslateonClick()

   @property
   def currentPen(self):
       return x2a.asyPen.fromAsyPen(self._currentPen)
       pass
   def debug(self):
       print('Put a breakpoint here.')

   def execPythonCmd(self):
       commandText, result = Qw.QInputDialog.getText(self, '', 'enter python cmd')
       if result:
           exec(commandText)

   def deleteAddOptions(self):
       if self.currAddOptionsWgt is not None:
           self.currAddOptionsWgt.hide()
           self.ui.addOptionLayout.removeWidget(self.currAddOptionsWgt)
           self.currAddOptionsWgt = None

   def updateOptionWidget(self):
       try:
           self.addMode.objectCreated.disconnect()
       except Exception:
           pass

       #self.currentModeStack[-1] = None
       self.addMode.objectCreated.connect(self.addInPlace)
       self.updateModeBtnsOnly()


       self.deleteAddOptions()

       self.currAddOptionsWgt = self.addMode.createOptWidget(self.currAddOptions)
       if self.currAddOptionsWgt is not None:
           self.ui.addOptionLayout.addWidget(self.currAddOptionsWgt)

   def addInPlace(self, obj):
       obj.asyengine = self.asyEngine
       if isinstance(obj, x2a.xasyText):
           obj.label.pen = self.currentPen
       else:
           obj.pen = self.currentPen
       obj.onCanvas = self.xasyDrawObj
       obj.setKey(str(self.globalObjectCounter))
       self.globalObjectCounter = self.globalObjectCounter + 1

       self.fileItems.append(obj)
       self.fileChanged = True
       self.addObjCreationUrs(obj)
       self.asyfyCanvas()

   def addObjCreationUrs(self, obj):
       newAction = self.createAction(ObjCreationChanges(obj))
       self.undoRedoStack.add(newAction)
       self.checkUndoRedoButtons()

   def clearGuides(self):
       self.currentGuides.clear()
       self.quickUpdate()

   LegacyHint='Click and drag to draw; right click or space bar to finalize'
   Hint='Click and drag to draw; release and click in place to add node; continue dragging'
   HintClose=' or c to close.'

   def drawHint(self):
       if self.settings['useLegacyDrawMode']:
           self.ui.statusbar.showMessage(self.LegacyHint+'.')
       else:
           self.ui.statusbar.showMessage(self.Hint+'.')

   def drawHintOpen(self):
       if self.settings['useLegacyDrawMode']:
           self.ui.statusbar.showMessage(self.LegacyHint+self.HintClose)
       else:
           self.ui.statusbar.showMessage(self.Hint+self.HintClose)

   def btnAddBezierInplaceOnClick(self):
       self.fileChanged = True
       self.addMode = InplaceAddObj.AddBezierShape(self)
       self.updateOptionWidget()

   def btnAddOpenLineOnClick(self):
       if self.currentModeStack[-1] != SelectionMode.openPoly:
           self.currentModeStack = [SelectionMode.openPoly]
           self.currAddOptions['useBezier'] = False
           self.currAddOptions['closedPath'] = False
           self.drawHintOpen()
           self.btnAddBezierInplaceOnClick()
       else:
           self.btnTranslateonClick()

   def btnAddClosedLineOnClick(self):
       if self.currentModeStack[-1] != SelectionMode.closedPoly:
           self.currentModeStack = [SelectionMode.closedPoly]
           self.currAddOptions['useBezier'] = False
           self.currAddOptions['closedPath'] = True
           self.drawHint()
           self.btnAddBezierInplaceOnClick()
       else:
           self.btnTranslateonClick()

   def btnAddOpenCurveOnClick(self):
       if self.currentModeStack[-1] != SelectionMode.openCurve:
           self.currentModeStack = [SelectionMode.openCurve]
           self.currAddOptions['useBezier'] = True
           self.currAddOptions['closedPath'] = False
           self.drawHintOpen()
           self.btnAddBezierInplaceOnClick()
       else:
           self.btnTranslateonClick()

   def btnAddClosedCurveOnClick(self):
       if self.currentModeStack[-1] != SelectionMode.closedCurve:
           self.currentModeStack = [SelectionMode.closedCurve]
           self.currAddOptions['useBezier'] = True
           self.currAddOptions['closedPath'] = True
           self.drawHint()
           self.btnAddBezierInplaceOnClick()
       else:
           self.btnTranslateonClick()

   def btnAddPolyOnClick(self):
       if self.currentModeStack[-1] != SelectionMode.addPoly:
           self.currentModeStack = [SelectionMode.addPoly]
           self.addMode = InplaceAddObj.AddPoly(self)
           self.ui.statusbar.showMessage('Add polygon on click')
           self.updateOptionWidget()
       else:
           self.btnTranslateonClick()

   def btnAddCircleOnClick(self):
       if self.currentModeStack[-1] != SelectionMode.addCircle:
           self.currentModeStack = [SelectionMode.addCircle]
           self.addMode = InplaceAddObj.AddCircle(self)
           self.ui.statusbar.showMessage('Add circle on click')
           self.updateOptionWidget()
       else:
           self.btnTranslateonClick()

   def btnAddLabelOnClick(self):
       if self.currentModeStack[-1] != SelectionMode.addLabel:
           self.currentModeStack = [SelectionMode.addLabel]
           self.addMode = InplaceAddObj.AddLabel(self)
           self.ui.statusbar.showMessage('Add label on click')
           self.updateOptionWidget()
       else:
           self.btnTranslateonClick()

   def btnAddFreehandOnClick(self):
       if self.currentModeStack[-1] != SelectionMode.addFreehand:
           self.currentModeStack = [SelectionMode.addFreehand]
           self.currAddOptions['useBezier'] = False
           self.currAddOptions['closedPath'] = False
           self.ui.statusbar.showMessage("Draw freehand")
           self.addMode = InplaceAddObj.AddFreehand(self)
           self.updateOptionWidget()
       else:
           self.btnTranslateonClick()

   def addTransformationChanges(self, objIndex, transform, isLocal=False):
       self.undoRedoStack.add(self.createAction(TransformationChanges(objIndex,
                           transform, isLocal)))
       self.checkUndoRedoButtons()

   def btnSendForwardsOnClick(self):
       if self.currentlySelectedObj['selectedIndex'] is not None:
           maj, minor = self.currentlySelectedObj['selectedIndex']
           selectedObj = self.drawObjects[maj][minor]
           index = self.fileItems.index(selectedObj.parent())

           self.clearSelection()
           if index == len(self.fileItems) - 1:
               return
           else:
               self.fileItems[index], self.fileItems[index + 1] = self.fileItems[index + 1], self.fileItems[index]
               self.asyfyCanvas()

   def btnSelectiveDeleteOnClick(self):
       if self.currentlySelectedObj['selectedIndex'] is not None:
           maj, minor = self.currentlySelectedObj['selectedIndex']
           selectedObj = self.drawObjects[maj][minor]

           parent = selectedObj.parent()

           if isinstance(parent, x2a.xasyScript):
               objKey=(selectedObj.key, selectedObj.keyIndex)
               self.hiddenKeys.add(objKey)
               self.undoRedoStack.add(self.createAction(
                   SoftDeletionChanges(selectedObj.parent(), objKey)
                   ))
               self.softDeleteObj((maj, minor))
           else:
               index = self.fileItems.index(selectedObj.parent())

               self.undoRedoStack.add(self.createAction(
                   HardDeletionChanges(selectedObj.parent(), index)
               ))

               self.fileItems.remove(selectedObj.parent())

           self.checkUndoRedoButtons()
           self.fileChanged = True
           self.clearSelection()
           self.asyfyCanvas()
       else:
           result = self.selectOnHover()
           if result:
               self.btnSelectiveDeleteOnClick()

   def btnSetVisibilityOnClick(self):
       if self.currentlySelectedObj['selectedIndex'] is not None:
           maj, minor = self.currentlySelectedObj['selectedIndex']
           selectedObj = self.drawObjects[maj][minor]

           self.hiddenKeys.symmetric_difference_update({(selectedObj.key, selectedObj.keyIndex)})
           self.clearSelection()
           self.quickUpdate()

   def btnSendBackwardsOnClick(self):
       if self.currentlySelectedObj['selectedIndex'] is not None:
           maj, minor = self.currentlySelectedObj['selectedIndex']
           selectedObj = self.drawObjects[maj][minor]
           index = self.fileItems.index(selectedObj.parent())

           self.clearSelection()
           if index == 0:
               return
           else:
               self.fileItems[index], self.fileItems[index - 1] = self.fileItems[index - 1], self.fileItems[index]
               self.asyfyCanvas()


   def btnUndoOnClick(self):
       if self.currentlySelectedObj['selectedIndex'] is not None:
           # avoid deleting currently selected object
           maj, minor = self.currentlySelectedObj['selectedIndex']
           selectedObj = self.drawObjects[maj][minor]
           if selectedObj != self.drawObjects[-1][0]:
               self.undoRedoStack.undo()
               self.checkUndoRedoButtons()
       else:
           self.undoRedoStack.undo()
           self.checkUndoRedoButtons()

   def btnRedoOnClick(self):
       self.undoRedoStack.redo()
       self.checkUndoRedoButtons()

   def checkUndoRedoButtons(self):
       self.ui.btnUndo.setEnabled(self.undoRedoStack.changesMade())
       self.ui.actionUndo.setEnabled(self.undoRedoStack.changesMade())

       self.ui.btnRedo.setEnabled(len(self.undoRedoStack.redoStack) > 0)
       self.ui.actionRedo.setEnabled(len(self.undoRedoStack.redoStack) > 0)

   def handleUndoChanges(self, change):
       assert isinstance(change, ActionChanges)
       if isinstance(change, TransformationChanges):
           self.transformObject(change.objIndex, change.transformation.inverted(), change.isLocal)
       elif isinstance(change, ObjCreationChanges):
           self.fileItems.pop()
       elif isinstance(change, HardDeletionChanges):
           self.fileItems.insert(change.objIndex, change.item)
       elif isinstance(change, SoftDeletionChanges):
           key, keyIndex = change.keyMap
           self.hiddenKeys.remove((key, keyIndex))
           change.item.transfKeymap[key][keyIndex].deleted = False
       elif isinstance(change, EditBezierChanges):
           self.fileItems[change.objIndex].path = change.oldPath
       self.asyfyCanvas()

   def handleRedoChanges(self, change):
       assert isinstance(change, ActionChanges)
       if isinstance(change, TransformationChanges):
           self.transformObject(
                change.objIndex, change.transformation, change.isLocal)
       elif isinstance(change, ObjCreationChanges):
           self.fileItems.append(change.object)
       elif isinstance(change, HardDeletionChanges):
           self.fileItems.remove(change.item)
       elif isinstance(change, SoftDeletionChanges):
           key, keyIndex = change.keyMap
           self.hiddenKeys.add((key, keyIndex))
           change.item.transfKeymap[key][keyIndex].deleted = True
       elif isinstance(change, EditBezierChanges):
           self.fileItems[change.objIndex].path = change.newPath
       self.asyfyCanvas()

   #  is this a "pythonic" way?
   def createAction(self, changes):
       def _change():
           return self.handleRedoChanges(changes)

       def _undoChange():
           return self.handleUndoChanges(changes)

       return Urs.action((_change, _undoChange))

   def execCustomCommand(self, command):
       if command in self.commandsFunc:
           self.commandsFunc[command]()
       else:
           self.ui.statusbar.showMessage('Command {0} not found'.format(command))

   def enterCustomCommand(self):
       commandText, result = Qw.QInputDialog.getText(self, 'Enter Custom Command', 'Enter Custom Command')
       if result:
           self.execCustomCommand(commandText)

   def addXasyShapeFromPath(self, path, pen = None, transform = x2a.identity(), key = None, fill = False):
       dashPattern = pen['dashPattern'] #?
       if not pen:
           pen = self.currentPen
       else:
           pen = x2a.asyPen(self.asyEngine, color = pen['color'], width = pen['width'], pen_options = pen['options'])
           if dashPattern:
               pen.setDashPattern(dashPattern)

       newItem = x2a.xasyShape(path, self.asyEngine, pen = pen, transform = transform)
       if fill:
           newItem.swapFill()
       newItem.setKey(key)
       self.fileItems.append(newItem)

   def addXasyArrowFromPath(self, pen, transform, key, arrowSettings, code, dashPattern = None):
       if not pen:
           pen = self.currentPen
       else:
           pen = x2a.asyPen(self.asyEngine, color = pen['color'], width = pen['width'], pen_options = pen['options'])
           if dashPattern:
               pen.setDashPattern(dashPattern)

       newItem = x2a.asyArrow(self.asyEngine, pen, transform, key, canvas=self.xasyDrawObj, code=code)
       newItem.setKey(key)
       newItem.arrowSettings = arrowSettings
       self.fileItems.append(newItem)

   def addXasyTextFromData(self, text, location, pen, transform, key, align, fontSize):
       if not pen:
           pen = self.currentPen
       else:
           pen = x2a.asyPen(self.asyEngine, color = pen['color'], width = pen['width'], pen_options = pen['options'])
       newItem = x2a.xasyText(text, location, self.asyEngine, pen, transform, key, align, fontSize)
       newItem.setKey(key)
       newItem.onCanvas = self.xasyDrawObj
       self.fileItems.append(newItem)


   def actionManual(self):
       asyManualURL = 'https://asymptote.sourceforge.io/asymptote.pdf'
       webbrowser.open_new(asyManualURL)

   def actionAbout(self):
       Qw.QMessageBox.about(self,"xasy","This is xasy "+xasyVersion+"; a graphical front end to the Asymptote vector graphics language: https://asymptote.sourceforge.io/")

   def actionExport(self, pathToFile):
       asyFile = io.open(os.path.realpath(pathToFile), 'w')
       xf.saveFile(asyFile, self.fileItems, self.asy2psmap)
       asyFile.close()
       self.ui.statusbar.showMessage(f"Exported to '{pathToFile}' as an Asymptote file.")

   def btnExportToAsyOnClick(self):
       if self.fileName:
           pathToFile = os.path.splitext(self.fileName)[0]+'.asy'
       else:
           self.btnExportAsymptoteOnClick()
           return
       if os.path.isfile(pathToFile):
           reply = Qw.QMessageBox.question(self, 'Message',
               f'"{os.path.split(pathToFile)[1]}" already exists.  Do you want to overwrite it?',
               Qw.QMessageBox.Yes, Qw.QMessageBox.No)
           if reply == Qw.QMessageBox.No:
               return
           self.actionExport(pathToFile)

   def btnExportAsymptoteOnClick(self):
       diag = Qw.QFileDialog(self)
       diag.setAcceptMode(Qw.QFileDialog.AcceptSave)

       formatId = {
           'asy': {
               'name': 'Asymptote Files',
               'ext': ['*.asy']
           },
           'pdf': {
               'name': 'PDF Files',
               'ext': ['*.pdf']
           },
           'svg': {
               'name': 'Scalable Vector Graphics',
               'ext': ['*.svg']
           },
           'eps': {
               'name': 'Postscript Files',
               'ext': ['*.eps']
           },
           'png': {
               'name': 'Portable Network Graphics',
               'ext': ['*.png']
           },
           '*': {
               'name': 'Any Files',
               'ext': ['*.*']
           }
       }

       formats = ['asy', 'pdf', 'svg', 'eps', 'png', '*']

       formatText = ';;'.join('{0:s} ({1:s})'.format(formatId[form]['name'], ' '.join(formatId[form]['ext']))
                              for form in formats)

       if self.currDir is not None:
           diag.setDirectory(self.currDir)
           rawFile = os.path.splitext(os.path.basename(self.fileName))[0] + '.asy'
           diag.selectFile(rawFile)

       diag.setNameFilter(formatText)
       diag.show()
       result = diag.exec_()

       if result != diag.Accepted:
           return

       finalFiles = diag.selectedFiles()
       finalString = xf.xasy2asyCode(self.fileItems, self.asy2psmap)

       for file in finalFiles:
           ext = os.path.splitext(file)
           if len(ext) < 2:
               ext = 'asy'
           else:
               ext = ext[1][1:]
           if ext == '':
               ext='asy'
           if ext == 'asy':
               pathToFile = os.path.splitext(file)[0]+'.'+ext
               self.updateScript()
               self.actionExport(pathToFile)
           else:
               with subprocess.Popen(args=[self.asyPath, '-f{0}'.format(ext), '-o{0}'.format(file), '-'], encoding='utf-8',
                                   stdin=subprocess.PIPE) as asy:

                   asy.stdin.write(finalString)
                   asy.stdin.close()
                   asy.wait(timeout=35)

   def actionExportXasy(self, file):
       xasyObjects, asyItems = xf.xasyToDict(self.fileName, self.fileItems, self.asy2psmap)

       if asyItems:

           # Save imported items into the twin asy file
           asyScriptItems = [item['item'] for item in asyItems if item['type'] == 'xasyScript']

           prefix = os.path.splitext(self.fileName)[0]
           asyFilePath = prefix + '.asy'

           saveAsyFile = io.open(asyFilePath, 'w')
           xf.saveFile(saveAsyFile, asyScriptItems, self.asy2psmap)
           saveAsyFile.close()
           self.updateScript()

       openFile = open(file, 'wb')
       pickle.dump(xasyObjects, openFile)
       openFile.close()

   def actionLoadXasy(self, file):
       self.erase()
       self.ui.statusbar.showMessage('Load {0}'.format(file)) # TODO: This doesn't show on the UI
       self.fileName = file
       self.currDir = os.path.dirname(self.fileName)

       input_file = open(file, 'rb')
       xasyObjects = pickle.load(input_file)
       input_file.close()

       prefix = os.path.splitext(self.fileName)[0]
       asyFilePath = prefix + '.asy'
       rawText = None
       existsAsy = False

       if os.path.isfile(asyFilePath):
           asyFile = io.open(asyFilePath, 'r')
           rawText = asyFile.read()
           asyFile.close()
           rawText, transfDict = xf.extractTransformsFromFile(rawText)
           obj = x2a.xasyScript(canvas=self.xasyDrawObj, engine=self.asyEngine, transfKeyMap=transfDict)
           obj.setScript(rawText)
           self.fileItems.append(obj)
           existsAsy = True

       self.asyfyCanvas(force=True)

       for item in xasyObjects['objects']:
           key=item['transfKey']
           if existsAsy:
               if(key) in obj.transfKeymap.keys():
                   continue
               obj.maxKey=max(obj.maxKey,int(key))
           if item['type'] == 'xasyScript':
               print("Uh oh, there should not be any asy objects loaded")

           elif item['type'] == 'xasyText':
               self.addXasyTextFromData( text = item['text'],
                       location = item['location'], pen = None,
                       transform = x2a.asyTransform(item['transform']), key = item['transfKey'],
                       align = item['align'], fontSize = item['fontSize']
                       )

           elif item['type'] == 'xasyShape':
               nodeSet = item['nodes']
               linkSet = item['links']
               path = x2a.asyPath(self.asyEngine)
               path.initFromNodeList(nodeSet, linkSet)
               self.addXasyShapeFromPath(path, pen = item['pen'], transform = x2a.asyTransform(item['transform']), key = item['transfKey'], fill = item['fill'])

           elif item['type'] == 'asyArrow':
               self.addXasyArrowFromPath(item['pen'], x2a.asyTransform(item['transform']), item['transfKey'], item['settings'], item['code'])
               #self.addXasyArrowFromPath(item['oldpath'], item['pen'], x2a.asyTransform(item['transform']), item['transfKey'], item['settings'])

           else:
               print("ERROR")

       self.asy2psmap = x2a.asyTransform(xasyObjects['asy2psmap'])
       if existsAsy:
           self.globalObjectCounter = obj.maxKey+1

       self.asyfyCanvas()

       if existsAsy:
           self.ui.statusbar.showMessage(f"Corresponding Asymptote File '{os.path.basename(asyFilePath)}' found.  Loaded both files.")
       else:
           self.ui.statusbar.showMessage("No Asymptote file found.  Loaded exclusively GUI objects.")

   def loadKeyMaps(self):
       """Inverts the mapping of the key
          Input map is in format 'Action' : 'Key Sequence' """
       for action, key in self.keyMaps.options.items():
           shortcut = Qw.QShortcut(self)
           shortcut.setKey(Qg.QKeySequence(key))

           # hate doing this, but python doesn't have explicit way to pass a
           # string to a lambda without an identifier
           # attached to it.
           exec('shortcut.activated.connect(lambda: self.execCustomCommand("{0}"))'.format(action),
                {'self': self, 'shortcut': shortcut})

   def initializeButtons(self):
       self.ui.btnDrawAxes.setChecked(self.settings['defaultShowAxes'])
       self.btnDrawAxesOnClick(self.settings['defaultShowAxes'])

       self.ui.btnDrawGrid.setChecked(self.settings['defaultShowGrid'])
       self.btnDrawGridOnClick(self.settings['defaultShowGrid'])

   def erase(self):
       self.fileItems.clear()
       self.hiddenKeys.clear()
       self.undoRedoStack.clear()
       self.checkUndoRedoButtons()
       self.fileChanged = False

   #We include this function to keep the general program flow consistent
   def closeEvent(self, event):
       if self.actionClose() == Qw.QMessageBox.Cancel:
           event.ignore()

   def actionNewFile(self):
       if self.fileChanged:
           reply = self.saveDialog()
           if reply == Qw.QMessageBox.Yes:
               self.actionSave()
           elif reply == Qw.QMessageBox.Cancel:
               return
       self.erase()
       self.asyfyCanvas(force=True)
       self.fileName = None
       self.updateTitle()


   def actionOpen(self, fileName = None):
       if self.fileChanged:
           reply = self.saveDialog()
           if reply == Qw.QMessageBox.Yes:
               self.actionSave()
           elif reply == Qw.QMessageBox.Cancel:
               return

       if fileName:
           # Opening via open recent or cmd args
           _, file_extension = os.path.splitext(fileName)
           if file_extension == '.xasy':
               self.actionLoadXasy(fileName)
           else:
               self.loadFile(fileName)
           self.populateOpenRecent(fileName)
       else:
           filename = Qw.QFileDialog.getOpenFileName(self, 'Open Xasy/Asymptote File','', '(*.xasy *.asy)')
           if filename[0]:
               _, file_extension = os.path.splitext(filename[0])
               if file_extension == '.xasy':
                   self.actionLoadXasy(filename[0])
               else:
                   self.loadFile(filename[0])

           self.populateOpenRecent(filename[0].strip())

   def actionClearRecent(self):
       self.ui.menuOpenRecent.clear()
       self.openRecent.clear()
       self.ui.menuOpenRecent.addAction("Clear", self.actionClearRecent)

   def populateOpenRecent(self, recentOpenedFile = None):
       self.ui.menuOpenRecent.clear()
       if recentOpenedFile:
           self.openRecent.insert(recentOpenedFile)
       for count, path in enumerate(self.openRecent.pathList):
           if count > 8:
               break
           action = Qw.QAction(path, self, triggered = lambda state, path = path: self.actionOpen(fileName = path))
           self.ui.menuOpenRecent.addAction(action)
       self.ui.menuOpenRecent.addSeparator()
       self.ui.menuOpenRecent.addAction("Clear", self.actionClearRecent)

   def saveDialog(self) -> bool:
       save = "Save current file?"
       replyBox = Qw.QMessageBox()
       replyBox.setText("Save current file?")
       replyBox.setWindowTitle("Message")
       replyBox.setStandardButtons(Qw.QMessageBox.Yes | Qw.QMessageBox.No | Qw.QMessageBox.Cancel)
       reply = replyBox.exec()

       return reply

   def actionClose(self):
       if self.fileChanged:
           reply = self.saveDialog()
           if reply == Qw.QMessageBox.Yes:
               self.actionSave()
               Qc.QCoreApplication.quit()
           elif reply == Qw.QMessageBox.No:
               Qc.QCoreApplication.quit()
           else:
               return reply
       else:
           Qc.QCoreApplication.quit()

   def actionSave(self):
       if self.fileName is None:
           self.actionSaveAs()

       else:
           _, file_extension = os.path.splitext(self.fileName)
           if file_extension == ".asy":
               if self.existsXasy():
                   warning = "Choose save format. Note that objects saved in asy format cannot be edited graphically."
                   replyBox = Qw.QMessageBox()
                   replyBox.setWindowTitle('Warning')
                   replyBox.setText(warning)
                   replyBox.addButton("Save as .xasy", replyBox.NoRole)
                   replyBox.addButton("Save as .asy", replyBox.YesRole)
                   replyBox.addButton(Qw.QMessageBox.Cancel)
                   reply = replyBox.exec()
                   if reply == 1:
                       saveFile = io.open(self.fileName, 'w')
                       xf.saveFile(saveFile, self.fileItems, self.asy2psmap)
                       saveFile.close()
                       self.ui.statusbar.showMessage('File saved as {}'.format(self.fileName))
                       self.fileChanged = False
                   elif reply == 0:
                       prefix = os.path.splitext(self.fileName)[0]
                       xasyFilePath = prefix + '.xasy'
                       if os.path.isfile(xasyFilePath):
                           warning = f'"{os.path.basename(xasyFilePath)}" already exists.  Do you want to overwrite it?'
                           reply = Qw.QMessageBox.question(self, "Same File", warning, Qw.QMessageBox.No, Qw.QMessageBox.Yes)
                           if reply == Qw.QMessageBox.No:
                               return

                       self.actionExportXasy(xasyFilePath)
                       self.fileName = xasyFilePath
                       self.ui.statusbar.showMessage('File saved as {}'.format(self.fileName))
                       self.fileChanged = False
                   else:
                       return

               else:
                   saveFile = io.open(self.fileName, 'w')
                   xf.saveFile(saveFile, self.fileItems, self.asy2psmap)
                   saveFile.close()
                   self.fileChanged = False
           elif file_extension == ".xasy":
               self.actionExportXasy(self.fileName)
               self.ui.statusbar.showMessage('File saved as {}'.format(self.fileName))
               self.fileChanged = False
           else:
               print("ERROR: file extension not supported")
           self.updateScript()
           self.updateTitle()

   def updateScript(self):
       for item in self.fileItems:
           if isinstance(item, x2a.xasyScript):
               if item.updatedCode:
                   item.setScript(item.updatedCode)
                   item.updatedCode = None

   def existsXasy(self):
       for item in self.fileItems:
           if not isinstance(item, x2a.xasyScript):
               return True
       return False

   def actionSaveAs(self):
       initSave = os.path.splitext(str(self.fileName))[0]+'.xasy'
       saveLocation = Qw.QFileDialog.getSaveFileName(self, 'Save File', initSave, "Xasy File (*.xasy)")[0]
       if saveLocation:
           _, file_extension = os.path.splitext(saveLocation)
           if not file_extension:
               saveLocation += '.xasy'
               self.actionExportXasy(saveLocation)
           elif file_extension == ".xasy":
               self.actionExportXasy(saveLocation)
           else:
               print("ERROR: file extension not supported")
           self.fileName = saveLocation
           self.updateScript()
           self.fileChanged = False
           self.updateTitle()
           self.populateOpenRecent(saveLocation)


   def btnQuickScreenshotOnClick(self):
       saveLocation = Qw.QFileDialog.getSaveFileName(self, 'Save Screenshot','')
       if saveLocation[0]:
           self.ui.imgLabel.pixmap().save(saveLocation[0])

   def btnLoadFileonClick(self):
       self.actionOpen()

   def btnCloseFileonClick(self):
       self.actionClose()

   def btnSaveonClick(self):
       self.actionSave()

   @Qc.pyqtSlot(int)
   def handleAnchorComboIndex(self, index: int):
       self.anchorMode = index
       if self.anchorMode == AnchorMode.customAnchor:
           if self.customAnchor is not None:
               self.anchorMode = AnchorMode.customAnchor
           else:
               self.ui.comboAnchor.setCurrentIndex(AnchorMode.center)
               self.anchorMode = AnchorMode.center
       self.quickUpdate()
   def btnColorSelectOnClick(self):
       self.colorDialog.show()
       result = self.colorDialog.exec()
       if result == Qw.QDialog.Accepted:
           self._currentPen.setColorFromQColor(self.colorDialog.selectedColor())
           self.updateFrameDispColor()

   def txtLineWidthEdited(self, text):
       new_val = xu.tryParse(text, float)
       if new_val is not None:
           if new_val > 0:
               self._currentPen.setWidth(new_val)

   def isReady(self):
       return self.mainCanvas is not None

   def resizeEvent(self, resizeEvent):
       # super().resizeEvent(resizeEvent)
       assert isinstance(resizeEvent, Qg.QResizeEvent)

       if self.isReady():
           if self.mainCanvas.isActive():
               self.mainCanvas.end()
           self.canvSize = self.ui.imgFrame.size()*devicePixelRatio
           self.ui.imgFrame.setSizePolicy(Qw.QSizePolicy.Ignored, Qw.QSizePolicy.Ignored)
           self.canvasPixmap = Qg.QPixmap(self.canvSize)
           self.canvasPixmap.setDevicePixelRatio(devicePixelRatio)
           self.postCanvasPixmap = Qg.QPixmap(self.canvSize)
           self.canvasPixmap.setDevicePixelRatio(devicePixelRatio)

           self.quickUpdate()

   def show(self):
       super().show()
       self.createMainCanvas()  # somehow, the coordinates doesn't get updated until after showing.
       self.initializeButtons()
       self.postShow()

   def postShow(self):
       self.handleArguments()

   def roundPositionSnap(self, oldPoint):
       minorGridSize = self.settings['gridMajorAxesSpacing'] / (self.settings['gridMinorAxesCount'] + 1)
       if isinstance(oldPoint, list) or isinstance(oldPoint, tuple):
           return [round(val / minorGridSize) * minorGridSize for val in oldPoint]
       elif isinstance(oldPoint, Qc.QPoint) or isinstance(oldPoint, Qc.QPointF):
           x, y = oldPoint.x(), oldPoint.y()
           x = round(x / minorGridSize) * minorGridSize
           y = round(y / minorGridSize) * minorGridSize
           return Qc.QPointF(x, y)
       else:
           raise Exception

   def getAsyCoordinates(self):
       canvasPosOrig = self.getCanvasCoordinates()
       return canvasPosOrig, canvasPosOrig

   def mouseMoveEvent(self, mouseEvent: Qg.QMouseEvent):  # TODO: Actually refine grid snapping...
       if not self.ui.imgLabel.underMouse() and not self.mouseDown:
           return

       self.updateMouseCoordLabel()
       asyPos, canvasPos = self.getAsyCoordinates()

       # add mode
       if self.addMode is not None:
           if self.addMode.active:
               self.addMode.mouseMove(asyPos, mouseEvent)
               self.quickUpdate()
           return

       # pan mode
       if self.currentModeStack[-1] == SelectionMode.pan and int(mouseEvent.buttons()) and self.savedWindowMousePos is not None:
           mousePos = self.getWindowCoordinates()
           newPos = mousePos - self.savedWindowMousePos

           tx, ty = newPos.x(), newPos.y()

           if self.lockX:
               tx = 0
           if self.lockY:
               ty = 0

           self.panOffset[0] += tx
           self.panOffset[1] += ty

           self.savedWindowMousePos = self.getWindowCoordinates()
           self.quickUpdate()
           return

       # otherwise, in transformation
       if self.inMidTransformation:
           if self.currentModeStack[-1] == SelectionMode.translate:
               newPos = canvasPos - self.savedMousePosition
               if self.gridSnap:
                   newPos = self.roundPositionSnap(newPos)  # actually round to the nearest minor grid afterwards...

               self.tx, self.ty = newPos.x(), newPos.y()

               if self.lockX:
                   self.tx = 0
               if self.lockY:
                   self.ty = 0
               self.newTransform = Qg.QTransform.fromTranslate(self.tx, self.ty)

           elif self.currentModeStack[-1] == SelectionMode.rotate:
               if self.gridSnap:
                   canvasPos = self.roundPositionSnap(canvasPos)

               adjustedSavedMousePos = self.savedMousePosition - self.currentAnchor
               adjustedCanvasCoords = canvasPos - self.currentAnchor

               origAngle = np.arctan2(adjustedSavedMousePos.y(), adjustedSavedMousePos.x())
               newAng = np.arctan2(adjustedCanvasCoords.y(), adjustedCanvasCoords.x())
               self.deltaAngle = newAng - origAngle
               self.newTransform = xT.makeRotTransform(self.deltaAngle, self.currentAnchor).toQTransform()

           elif self.currentModeStack[-1] == SelectionMode.scale:
               if self.gridSnap:
                   canvasPos = self.roundPositionSnap(canvasPos)
                   x, y = int(round(canvasPos.x())), int(round(canvasPos.y()))  # otherwise it crashes...
                   canvasPos = Qc.QPoint(x, y)

               originalDeltaPts = self.savedMousePosition - self.currentAnchor
               scaleFactor = Qc.QPointF.dotProduct(canvasPos - self.currentAnchor, originalDeltaPts) /\
                   (xu.twonorm((originalDeltaPts.x(), originalDeltaPts.y())) ** 2)
               if not self.lockX:
                   self.scaleFactorX = scaleFactor
               else:
                   self.scaleFactorX = 1

               if not self.lockY:
                   self.scaleFactorY = scaleFactor
               else:
                   self.scaleFactorY = 1

               self.newTransform = xT.makeScaleTransform(self.scaleFactorX, self.scaleFactorY, self.currentAnchor).\
                   toQTransform()

           self.quickUpdate()
           return

       # otherwise, select a candidate for selection

       if self.currentlySelectedObj['selectedIndex'] is None:
           selectedIndex, selKeyList = self.selectObject()
           if selectedIndex is not None:
               if self.pendingSelectedObjList != selKeyList:
                   self.pendingSelectedObjList = selKeyList
                   self.pendingSelectedObjIndex = -1
           else:
               self.pendingSelectedObjList.clear()
               self.pendingSelectedObjIndex = -1
           self.quickUpdate()
           return


   def mouseReleaseEvent(self, mouseEvent):
       assert isinstance(mouseEvent, Qg.QMouseEvent)
       if not self.mouseDown:
           return

       self.tx=0
       self.ty=0
       self.mouseDown = False
       if self.addMode is not None:
           self.addMode.mouseRelease()
       if self.inMidTransformation:
           self.clearSelection()
       self.inMidTransformation = False
       self.quickUpdate()

   def clearSelection(self):
       if self.currentlySelectedObj['selectedIndex'] is not None:
           self.releaseTransform()
       self.setAllInSetEnabled(self.objButtons, False)
       self.currentlySelectedObj['selectedIndex'] = None
       self.currentlySelectedObj['key'] = None

       self.currentlySelectedObj['allSameKey'].clear()
       self.newTransform = Qg.QTransform()
       self.currentBoundingBox = None
       self.quickUpdate()

   def changeSelection(self, offset):
       if self.pendingSelectedObjList:
           if offset > 0:
               if self.pendingSelectedObjIndex + offset <= -1:
                   self.pendingSelectedObjIndex = self.pendingSelectedObjIndex + offset
           else:
               if self.pendingSelectedObjIndex + offset >= -len(self.pendingSelectedObjList):
                   self.pendingSelectedObjIndex = self.pendingSelectedObjIndex + offset

   def mouseWheel(self, rawAngleX: float, rawAngle: float, defaultModifiers: int=0):
       keyModifiers = int(Qw.QApplication.keyboardModifiers())
       keyModifiers = keyModifiers | defaultModifiers
       if keyModifiers & int(Qc.Qt.ControlModifier):
           oldMag = self.magnification
           factor = 0.5/devicePixelRatio
           cx, cy = self.canvSize.width()*factor, self.canvSize.height()*factor
           centerPoint = Qc.QPointF(cx, cy) * self.getScrsTransform().inverted()[0]

           self.magnification += (rawAngle/100)

           if self.magnification < self.settings['minimumMagnification']:
               self.magnification = self.settings['minimumMagnification']
           elif self.magnification > self.settings['maximumMagnification']:
               self.magnification = self.settings['maximumMagnification']

           # set the new pan. Let c be the fixed point (center point),
           # Let m the old mag, n the new mag

           # find t2 such that
           # mc + t1 = nc + t2 ==> t2 = (m - n)c + t1

           centerPoint = (oldMag - self.magnification) * centerPoint

           self.panOffset = [
               self.panOffset[0] + centerPoint.x(),
               self.panOffset[1] + centerPoint.y()
           ]

           self.currAddOptions['magnification'] = self.magnification

           if self.addMode is xbi.InteractiveBezierEditor:
               self.addMode.setSelectionBoundaries()

       elif keyModifiers & (int(Qc.Qt.ShiftModifier) | int(Qc.Qt.AltModifier)):
           self.panOffset[1] += rawAngle/1
           self.panOffset[0] -= rawAngleX/1
       # handle scrolling
       else:
           # process selection layer change
           if rawAngle >= 15:
               self.changeSelection(1)
           elif rawAngle <= -15:
               self.changeSelection(-1)
       self.quickUpdate()

   def wheelEvent(self, event: Qg.QWheelEvent):
       rawAngle = event.angleDelta().y() / 8
       rawAngleX = event.angleDelta().x() / 8
       self.mouseWheel(rawAngleX, rawAngle)

   def selectOnHover(self):
       """Returns True if selection happened, False otherwise.
       """
       if self.pendingSelectedObjList:
           selectedIndex = self.pendingSelectedObjList[self.pendingSelectedObjIndex]
           self.pendingSelectedObjList.clear()

           maj, minor = selectedIndex

           self.currentlySelectedObj['selectedIndex'] = selectedIndex
           self.currentlySelectedObj['key'],  self.currentlySelectedObj['allSameKey'] = self.selectObjectSet(
           )

           self.currentBoundingBox = self.drawObjects[maj][minor].boundingBox

           if self.selectAsGroup:
               for selItems in self.currentlySelectedObj['allSameKey']:
                   obj = self.drawObjects[selItems[0]][selItems[1]]
                   self.currentBoundingBox = self.currentBoundingBox.united(obj.boundingBox)

           self.origBboxTransform = self.drawObjects[maj][minor].transform.toQTransform()
           self.newTransform = Qg.QTransform()
           return True
       else:
           return False

   def mousePressEvent(self, mouseEvent: Qg.QMouseEvent):
       # we make an exception for bezier curve
       bezierException = False
       if self.addMode is not None:
           if self.addMode.active and isinstance(self.addMode, InplaceAddObj.AddBezierShape):
               bezierException = True

       if not self.ui.imgLabel.underMouse() and not bezierException:
           return

       self.mouseDown = True
       asyPos, self.savedMousePosition = self.getAsyCoordinates()

       if self.addMode is not None:
           self.addMode.mouseDown(asyPos, self.currAddOptions, mouseEvent)
       elif self.currentModeStack[-1] == SelectionMode.pan:
           self.savedWindowMousePos = self.getWindowCoordinates()
       elif self.currentModeStack[-1] == SelectionMode.setAnchor:
           self.customAnchor = self.savedMousePosition
           self.currentModeStack.pop()

           self.anchorMode = AnchorMode.customAnchor
           self.ui.comboAnchor.setCurrentIndex(AnchorMode.customAnchor)
           self.updateChecks()
           self.quickUpdate()
       elif self.inMidTransformation:
           pass
       elif self.pendingSelectedObjList:
           self.selectOnHover()

           if self.currentModeStack[-1] in {SelectionMode.translate, SelectionMode.rotate, SelectionMode.scale}:
               self.setAllInSetEnabled(self.objButtons, False)
               self.inMidTransformation = True
               self.setAnchor()
           elif self.currentModeStack[-1] == SelectionMode.delete:
               self.btnSelectiveDeleteOnClick()
           elif self.currentModeStack[-1] == SelectionMode.selectEdit:
               self.setupSelectEdit()
           else:
               self.setAllInSetEnabled(self.objButtons, True)
               self.inMidTransformation = False
               self.setAnchor()

       else:
           self.setAllInSetEnabled(self.objButtons, False)
           self.currentBoundingBox = None
           self.inMidTransformation = False
           self.clearSelection()

       self.quickUpdate()

   def removeAddMode(self):
       self.addMode = None
       self.deleteAddOptions()

   def editAccepted(self, obj, objIndex):
       self.undoRedoStack.add(self.createAction(
           EditBezierChanges(obj, objIndex,
                   self.addMode.asyPathBackup,
                   self.addMode.asyPath
           )
       ))
       self.checkUndoRedoButtons()

       self.addMode.forceFinalize()
       self.removeAddMode()
       self.fileChanged = True
       self.quickUpdate()

   def editRejected(self):
       self.addMode.resetObj()
       self.addMode.forceFinalize()
       self.removeAddMode()
       self.fileChanged = True
       self.quickUpdate()

   def setupSelectEdit(self):
       """For Select-Edit mode. For now, if the object selected is a bezier curve, opens up a bezier editor"""
       maj, minor = self.currentlySelectedObj['selectedIndex']
       obj = self.fileItems[maj]
       if isinstance(obj, x2a.xasyDrawnItem):
           # bezier path
           self.addMode = xbi.InteractiveBezierEditor(self, obj, self.currAddOptions)
           self.addMode.objectUpdated.connect(self.objectUpdated)
           self.addMode.editAccepted.connect(lambda: self.editAccepted(obj, maj))
           self.addMode.editRejected.connect(self.editRejected)
           self.updateOptionWidget()
           self.currentModeStack[-1] = SelectionMode.selectEdit
           self.fileChanged = True
       elif isinstance(obj, x2a.xasyText):
           newText = self.setTextPrompt()
           if newText:
               self.drawObjects.remove(obj.generateDrawObjects(False))
               obj.label.setText(newText)
               self.drawObjects.append(obj.generateDrawObjects(True))
               self.fileChanged = True
       else:
           self.ui.statusbar.showMessage('Warning: Selected object cannot be edited')
           self.clearSelection()
       self.quickUpdate()

   def setAnchor(self):
       if self.anchorMode == AnchorMode.center:
           self.currentAnchor = self.currentBoundingBox.center()
       elif self.anchorMode == AnchorMode.topLeft:
           self.currentAnchor = self.currentBoundingBox.topLeft()
       elif self.anchorMode == AnchorMode.topRight:
           self.currentAnchor = self.currentBoundingBox.topRight()
       elif self.anchorMode == AnchorMode.bottomLeft:
           self.currentAnchor = self.currentBoundingBox.bottomLeft()
       elif self.anchorMode == AnchorMode.bottomRight:
           self.currentAnchor = self.currentBoundingBox.bottomRight()
       elif self.anchorMode == AnchorMode.customAnchor:
           self.currentAnchor = self.customAnchor
       else:
           self.currentAnchor = Qc.QPointF(0, 0)

       if self.anchorMode != AnchorMode.origin:
           pass
           # TODO: Record base points/bbox before hand and use that for
           # anchor?
           # adjTransform =
           # self.drawObjects[selectedIndex].transform.toQTransform()
           # self.currentAnchor = adjTransform.map(self.currentAnchor)


   def releaseTransform(self):
       if self.newTransform.isIdentity():
           return
       newTransform = x2a.asyTransform.fromQTransform(self.newTransform)
       objKey = self.currentlySelectedObj['selectedIndex']
       self.addTransformationChanges(objKey, newTransform, not self.useGlobalCoords)
       self.transformObject(objKey, newTransform, not self.useGlobalCoords)

   def adjustTransform(self, appendTransform):
       self.screenTransformation = self.screenTransformation * appendTransform

   def createMainCanvas(self):
       self.canvSize = devicePixelRatio*self.ui.imgFrame.size()
       self.ui.imgFrame.setSizePolicy(Qw.QSizePolicy.Ignored, Qw.QSizePolicy.Ignored)
       factor=0.5/devicePixelRatio;
       x, y = self.canvSize.width()*factor, self.canvSize.height()*factor

       self.canvasPixmap = Qg.QPixmap(self.canvSize)
       self.canvasPixmap.setDevicePixelRatio(devicePixelRatio)

       self.canvasPixmap.fill()

       self.finalPixmap = Qg.QPixmap(self.canvSize)
       self.finalPixmap.setDevicePixelRatio(devicePixelRatio)

       self.postCanvasPixmap = Qg.QPixmap(self.canvSize)
       self.postCanvasPixmap.setDevicePixelRatio(devicePixelRatio)

       self.mainCanvas = Qg.QPainter(self.canvasPixmap)
       self.mainCanvas.setRenderHint(Qg.QPainter.Antialiasing)
       self.mainCanvas.setRenderHint(Qg.QPainter.SmoothPixmapTransform)
       self.mainCanvas.setRenderHint(Qg.QPainter.HighQualityAntialiasing)
       self.xasyDrawObj['canvas'] = self.mainCanvas

       self.mainTransformation = Qg.QTransform()
       self.mainTransformation.scale(1, 1)
       self.mainTransformation.translate(x, y)

       self.mainCanvas.setTransform(self.getScrsTransform(), True)

       self.ui.imgLabel.setPixmap(self.canvasPixmap)

   def resetPan(self):
       self.panOffset = [0, 0]
       self.quickUpdate()

   def btnPanCenterOnClick(self):
       newCenter = self.getAllBoundingBox().center()

       # adjust to new magnification
       # technically, doable through getscrstransform()
       # and subtract pan offset and center points
       # but it's much more work...
       newCenter = self.magnification * newCenter
       self.panOffset = [-newCenter.x(), -newCenter.y()]

       self.quickUpdate()

   def selectObject(self):
       if not self.ui.imgLabel.underMouse():
           return None, []
       canvasCoords = self.getCanvasCoordinates()
       highestDrawPriority = -np.inf
       collidedObjKey = None
       rawObjNumList = []
       for objKeyMaj in range(len(self.drawObjects)):
           for objKeyMin in range(len(self.drawObjects[objKeyMaj])):
               obj = self.drawObjects[objKeyMaj][objKeyMin]
               if obj.collide(canvasCoords) and (obj.key, obj.keyIndex) not in self.hiddenKeys:
                   rawObjNumList.append(((objKeyMaj, objKeyMin), obj.drawOrder))
                   if obj.drawOrder > highestDrawPriority:
                       collidedObjKey = (objKeyMaj, objKeyMin)
       if collidedObjKey is not None:
           rawKey = self.drawObjects[collidedObjKey[0]][collidedObjKey[1]].key
#            self.ui.statusbar.showMessage('Collide with {0}, Key is {1}'.format(str(collidedObjKey), rawKey), 2500)
           self.ui.statusbar.showMessage('Key: {0}'.format(rawKey), 2500)
           return collidedObjKey, [rawObj[0] for rawObj in sorted(rawObjNumList, key=lambda ordobj: ordobj[1])]
       else:
           return None, []

   def selectObjectSet(self):
       objKey = self.currentlySelectedObj['selectedIndex']
       if objKey is None:
           return set()
       assert isinstance(objKey, (tuple, list)) and len(objKey) == 2
       rawObj = self.drawObjects[objKey[0]][objKey[1]]
       rawKey = rawObj.key
       rawSet = {objKey}
       for objKeyMaj in range(len(self.drawObjects)):
           for objKeyMin in range(len(self.drawObjects[objKeyMaj])):
               obj = self.drawObjects[objKeyMaj][objKeyMin]
               if obj.key == rawKey:
                   rawSet.add((objKeyMaj, objKeyMin))
       return rawKey, rawSet

   def getCanvasCoordinates(self):
       # assert self.ui.imgLabel.underMouse()
       uiPos = self.mapFromGlobal(Qg.QCursor.pos())
       canvasPos = self.ui.imgLabel.mapFrom(self, uiPos)

       # Issue: For magnification, should xasy treats this at xasy level, or asy level?
       return canvasPos * self.getScrsTransform().inverted()[0]

   def getWindowCoordinates(self):
       # assert self.ui.imgLabel.underMouse()
       return self.mapFromGlobal(Qg.QCursor.pos())

   def refreshCanvas(self):
       if self.mainCanvas.isActive():
           self.mainCanvas.end()
       self.mainCanvas.begin(self.canvasPixmap)
       self.mainCanvas.setTransform(self.getScrsTransform())

   def asyfyCanvas(self, force=False):
       self.drawObjects = []
       self.populateCanvasWithItems(force)
       self.quickUpdate()
       if self.currentModeStack[-1] == SelectionMode.translate:
           self.ui.statusbar.showMessage(self.strings.asyfyComplete)

   def updateMouseCoordLabel(self):
       *args, canvasPos = self.getAsyCoordinates()
       nx, ny = self.asy2psmap.inverted() * (canvasPos.x(), canvasPos.y())
       self.coordLabel.setText('{0:.2f}, {1:.2f}    '.format(nx, ny))

   def quickUpdate(self):
       # TODO: Some documentation here would be nice since this is one of the
       # main functions that gets called everywhere.
       self.updateMouseCoordLabel()
       self.refreshCanvas()

       self.preDraw(self.mainCanvas) # coordinates/background
       self.quickDraw()

       self.mainCanvas.end()
       self.postDraw()
       self.updateScreen()

       self.updateTitle()

   def quickDraw(self):
       assert self.isReady()
       dpi = self.magnification * self.dpi
       activeItem = None
       for majorItem in self.drawObjects:
           for item in majorItem:
               # hidden objects - toggleable
               if (item.key, item.keyIndex) in self.hiddenKeys:
                   continue
               isSelected = item.key == self.currentlySelectedObj['key']
               if not self.selectAsGroup and isSelected and self.currentlySelectedObj['selectedIndex'] is not None:
                   maj, min_ = self.currentlySelectedObj['selectedIndex']
                   isSelected = isSelected and item is self.drawObjects[maj][min_]
               if isSelected and self.settings['enableImmediatePreview']:
                   activeItem = item
                   if self.useGlobalCoords:
                       item.draw(self.newTransform, canvas=self.mainCanvas, dpi=dpi)
                   else:
                       item.draw(self.newTransform, applyReverse=True, canvas=self.mainCanvas, dpi=dpi)
               else:
                   item.draw(canvas=self.mainCanvas, dpi=dpi)

       if self.settings['drawSelectedOnTop']:
           if self.pendingSelectedObjList:
               maj, minor = self.pendingSelectedObjList[self.pendingSelectedObjIndex]
               self.drawObjects[maj][minor].draw(canvas=self.mainCanvas, dpi=dpi)
           # and apply the preview too...
           elif activeItem is not None:
               if self.useGlobalCoords:
                   activeItem.draw(self.newTransform, canvas=self.mainCanvas, dpi=dpi)
               else:
                   activeItem.draw(self.newTransform, applyReverse=True, canvas=self.mainCanvas, dpi=dpi)
               activeItem = None

   def updateTitle(self):
       # TODO: Undo redo doesn't update appropriately. Have to find a fix for this.
       title = ''
       if self.fileName:
           title += os.path.basename(self.fileName)
       else:
           title += "[Not Saved]"
       if self.fileChanged:
           title += ' *'
       self.setWindowTitle(title)

   def updateScreen(self):
       self.finalPixmap = Qg.QPixmap(self.canvSize)
       self.finalPixmap.setDevicePixelRatio(devicePixelRatio)
       self.finalPixmap.fill(Qc.Qt.black)
       with Qg.QPainter(self.finalPixmap) as finalPainter:
           drawPoint = Qc.QPoint(0, 0)
           finalPainter.drawPixmap(drawPoint, self.canvasPixmap)
           finalPainter.drawPixmap(drawPoint, self.postCanvasPixmap)
       self.ui.imgLabel.setPixmap(self.finalPixmap)

   def drawCartesianGrid(self, preCanvas):
       majorGrid = self.settings['gridMajorAxesSpacing'] * self.asy2psmap.xx
       minorGridCount = self.settings['gridMinorAxesCount']

       majorGridCol = Qg.QColor(self.settings['gridMajorAxesColor'])
       minorGridCol = Qg.QColor(self.settings['gridMinorAxesColor'])

       panX, panY = self.panOffset

       factor=0.5/devicePixelRatio;
       cx, cy = self.canvSize.width()*factor, self.canvSize.height()*factor

       x_range = (cx + (2 * abs(panX)))/self.magnification
       y_range = (cy + (2 * abs(panY)))/self.magnification

       for x in np.arange(0, 2 * x_range + 1, majorGrid):  # have to do
           # this in two stages...
           preCanvas.setPen(minorGridCol)
           self.makePenCosmetic(preCanvas)
           for xMinor in range(1, minorGridCount + 1):
               xCoord = round(x + ((xMinor / (minorGridCount + 1)) * majorGrid))
               preCanvas.drawLine(Qc.QLine(xCoord, -9999, xCoord, 9999))
               preCanvas.drawLine(Qc.QLine(-xCoord, -9999, -xCoord, 9999))

       for y in np.arange(0, 2 * y_range + 1, majorGrid):
           preCanvas.setPen(minorGridCol)
           self.makePenCosmetic(preCanvas)
           for yMinor in range(1, minorGridCount + 1):
               yCoord = round(y + ((yMinor / (minorGridCount + 1)) * majorGrid))
               preCanvas.drawLine(Qc.QLine(-9999, yCoord, 9999, yCoord))
               preCanvas.drawLine(Qc.QLine(-9999, -yCoord, 9999, -yCoord))

           preCanvas.setPen(majorGridCol)
           self.makePenCosmetic(preCanvas)
           roundY = round(y)
           preCanvas.drawLine(Qc.QLine(-9999, roundY, 9999, roundY))
           preCanvas.drawLine(Qc.QLine(-9999, -roundY, 9999, -roundY))

       for x in np.arange(0, 2 * x_range + 1, majorGrid):
           preCanvas.setPen(majorGridCol)
           self.makePenCosmetic(preCanvas)
           roundX = round(x)
           preCanvas.drawLine(Qc.QLine(roundX, -9999, roundX, 9999))
           preCanvas.drawLine(Qc.QLine(-roundX, -9999, -roundX, 9999))

   def drawPolarGrid(self, preCanvas):
       center = Qc.QPointF(0, 0)
       majorGridCol = Qg.QColor(self.settings['gridMajorAxesColor'])
       minorGridCol = Qg.QColor(self.settings['gridMinorAxesColor'])
       majorGrid = self.settings['gridMajorAxesSpacing']
       minorGridCount = self.settings['gridMinorAxesCount']

       majorAxisAng = (np.pi/4)  # 45 degrees - for now.
       minorAxisCount = 2  # 15 degrees each

       subRadiusSize = int(round((majorGrid / (minorGridCount + 1))))
       subAngleSize = majorAxisAng / (minorAxisCount + 1)

       for radius in range(majorGrid, 9999 + 1, majorGrid):
           preCanvas.setPen(majorGridCol)
           preCanvas.drawEllipse(center, radius, radius)

           preCanvas.setPen(minorGridCol)

           for minorRing in range(minorGridCount):
               subRadius = round(radius - (subRadiusSize * (minorRing + 1)))
               preCanvas.drawEllipse(center, subRadius, subRadius)

       currAng = majorAxisAng
       while currAng <= (2 * np.pi):
           preCanvas.setPen(majorGridCol)
           p1 = center + (9999 * Qc.QPointF(np.cos(currAng), np.sin(currAng)))
           preCanvas.drawLine(Qc.QLineF(center, p1))

           preCanvas.setPen(minorGridCol)
           for minorAngLine in range(minorAxisCount):
               newAng = currAng - (subAngleSize * (minorAngLine + 1))
               p1 = center + (9999 * Qc.QPointF(np.cos(newAng), np.sin(newAng)))
               preCanvas.drawLine(Qc.QLineF(center, p1))

           currAng = currAng + majorAxisAng

   def preDraw(self, painter):
       self.canvasPixmap.fill()
       preCanvas = painter

       preCanvas.setTransform(self.getScrsTransform())

       if self.drawAxes:
           preCanvas.setPen(Qc.Qt.gray)
           self.makePenCosmetic(preCanvas)
           preCanvas.drawLine(Qc.QLine(-9999, 0, 9999, 0))
           preCanvas.drawLine(Qc.QLine(0, -9999, 0, 9999))

       if self.drawGrid:
           if self.drawGridMode == GridMode.cartesian:
               self.drawCartesianGrid(painter)
           elif self.drawGridMode == GridMode.polar:
               self.drawPolarGrid(painter)

       if self.currentGuides:
           for guide in self.currentGuides:
               guide.drawShape(preCanvas)
       # preCanvas.end()

   def drawAddModePreview(self, painter):
       if self.addMode is not None:
           if self.addMode.active:
               # Preview Object
               if self.addMode.getPreview() is not None:
                   painter.setPen(self.currentPen.toQPen())
                   painter.drawPath(self.addMode.getPreview())
               self.addMode.postDrawPreview(painter)


   def drawTransformPreview(self, painter):
       if self.currentBoundingBox is not None and self.currentlySelectedObj['selectedIndex'] is not None:
           painter.save()
           maj, minor = self.currentlySelectedObj['selectedIndex']
           selObj = self.drawObjects[maj][minor]
           self.makePenCosmetic(painter)
           if not self.useGlobalCoords:
               painter.save()
               painter.setTransform(
                   selObj.transform.toQTransform(), True)
               # painter.setTransform(selObj.baseTransform.toQTransform(), True)
               painter.setPen(Qc.Qt.gray)
               painter.drawLine(Qc.QLine(-9999, 0, 9999, 0))
               painter.drawLine(Qc.QLine(0, -9999, 0, 9999))
               painter.setPen(Qc.Qt.black)
               painter.restore()

               painter.setTransform(selObj.getInteriorScrTransform(
                   self.newTransform).toQTransform(), True)
               painter.drawRect(selObj.localBoundingBox)
           else:
               painter.setTransform(self.newTransform, True)
               painter.drawRect(self.currentBoundingBox)
           painter.restore()

   def postDraw(self):
       self.postCanvasPixmap.fill(Qc.Qt.transparent)
       with Qg.QPainter(self.postCanvasPixmap) as postCanvas:
           postCanvas.setRenderHints(self.mainCanvas.renderHints())
           postCanvas.setTransform(self.getScrsTransform())
           self.makePenCosmetic(postCanvas)

           self.drawTransformPreview(postCanvas)

           if self.pendingSelectedObjList:
               maj, minor = self.pendingSelectedObjList[self.pendingSelectedObjIndex]
               postCanvas.drawRect(self.drawObjects[maj][minor].boundingBox)

           self.drawAddModePreview(postCanvas)

           if self.customAnchor is not None and self.anchorMode == AnchorMode.customAnchor:
               self.drawAnchorCursor(postCanvas)

           # postCanvas.drawRect(self.getAllBoundingBox())

   def drawAnchorCursor(self, painter):
       painter.drawEllipse(self.customAnchor, 6, 6)
       newCirclePath = Qg.QPainterPath()
       newCirclePath.addEllipse(self.customAnchor, 2, 2)

       painter.fillPath(newCirclePath, Qg.QColor.fromRgb(0, 0, 0))

   def updateModeBtnsOnly(self):
       if self.currentModeStack[-1] == SelectionMode.translate:
           activeBtn = self.ui.btnTranslate
       elif self.currentModeStack[-1] == SelectionMode.rotate:
           activeBtn = self.ui.btnRotate
       elif self.currentModeStack[-1] == SelectionMode.scale:
           activeBtn = self.ui.btnScale
       elif self.currentModeStack[-1] == SelectionMode.pan:
           activeBtn = self.ui.btnPan
       elif self.currentModeStack[-1] == SelectionMode.setAnchor:
           activeBtn = self.ui.btnAnchor
       elif self.currentModeStack[-1] == SelectionMode.delete:
           activeBtn = self.ui.btnDeleteMode
       elif self.currentModeStack[-1] == SelectionMode.selectEdit:
           activeBtn = self.ui.btnSelectEdit
       elif self.currentModeStack[-1] == SelectionMode.openPoly:
           activeBtn = self.ui.btnOpenPoly
       elif self.currentModeStack[-1] == SelectionMode.closedPoly:
           activeBtn = self.ui.btnClosedPoly
       elif self.currentModeStack[-1] == SelectionMode.openCurve:
           activeBtn = self.ui.btnOpenCurve
       elif self.currentModeStack[-1] == SelectionMode.closedCurve:
           activeBtn = self.ui.btnClosedCurve
       elif self.currentModeStack[-1] == SelectionMode.addPoly:
           activeBtn = self.ui.btnAddPoly
       elif self.currentModeStack[-1] == SelectionMode.addCircle:
           activeBtn = self.ui.btnAddCircle
       elif self.currentModeStack[-1] == SelectionMode.addLabel:
           activeBtn = self.ui.btnAddLabel
       elif self.currentModeStack[-1] == SelectionMode.addFreehand:
           activeBtn = self.ui.btnAddFreehand
       else:
           activeBtn = None


       disableFill = isinstance(self.addMode, InplaceAddObj.AddBezierShape) and not self.currAddOptions['closedPath']
       if isinstance(self.addMode, xbi.InteractiveBezierEditor):
           disableFill = disableFill or not (self.addMode.obj.path.nodeSet[-1] == "cycle")
       self.ui.btnFill.setEnabled(not disableFill)
       if disableFill and self.ui.btnFill.isEnabled():
           self.ui.btnFill.setChecked(not disableFill)


       for button in self.modeButtons:
           button.setChecked(button is activeBtn)

       if activeBtn in [self.ui.btnDeleteMode,self.ui.btnSelectEdit]:
           self.ui.btnAlignX.setEnabled(False)
           self.ui.btnAlignY.setEnabled(False)
       else:
           self.ui.btnAlignX.setEnabled(True)
           self.ui.btnAlignY.setEnabled(True)


   def updateChecks(self):
       self.removeAddMode()
       self.updateModeBtnsOnly()
       self.quickUpdate()

   def btnAlignXOnClick(self, checked):
       if self.currentModeStack[0] in [SelectionMode.selectEdit,SelectionMode.delete]:
           self.ui.btnAlignX.setChecked(False)
       else:
           self.lockY = checked
           if self.lockX:
               self.lockX = False
               self.ui.btnAlignY.setChecked(False)

   def btnAlignYOnClick(self, checked):
       if self.currentModeStack[0] in [SelectionMode.selectEdit,SelectionMode.delete]:
           self.ui.btnAlignY.setChecked(False)
       else:
           self.lockX = checked
           if self.lockY:
               self.lockY = False
               self.ui.btnAlignX.setChecked(False)

   def btnAnchorModeOnClick(self):
       if self.currentModeStack[-1] != SelectionMode.setAnchor:
           self.currentModeStack.append(SelectionMode.setAnchor)
           self.updateChecks()

   def switchToAnchorMode(self):
       if self.currentModeStack[-1] != SelectionMode.setAnchor:
           self.currentModeStack.append(SelectionMode.setAnchor)
           self.updateChecks()

   def btnTranslateonClick(self):
       self.currentModeStack = [SelectionMode.translate]
       self.ui.statusbar.showMessage('Translate mode')
       self.clearSelection()
       self.updateChecks()

   def btnRotateOnClick(self):
       if self.currentModeStack[-1] != SelectionMode.rotate:
           self.currentModeStack = [SelectionMode.rotate]
           self.ui.statusbar.showMessage('Rotate mode')
           self.clearSelection()
           self.updateChecks()
       else:
           self.btnTranslateonClick()

   def btnScaleOnClick(self):
       if self.currentModeStack[-1] != SelectionMode.scale:
           self.currentModeStack = [SelectionMode.scale]
           self.ui.statusbar.showMessage('Scale mode')
           self.clearSelection()
           self.updateChecks()
       else:
           self.btnTranslateonClick()

   def btnPanOnClick(self):
       if self.currentModeStack[-1] != SelectionMode.pan:
           self.currentModeStack = [SelectionMode.pan]
           self.ui.statusbar.showMessage('Pan mode')
           self.clearSelection()
           self.updateChecks()
       else:
           self.btnTranslateonClick()

   def btnWorldCoordsOnClick(self, checked):
       self.useGlobalCoords = checked
       if not self.useGlobalCoords:
           self.ui.comboAnchor.setCurrentIndex(AnchorMode.origin)
       self.setAllInSetEnabled(self.globalTransformOnlyButtons, checked)

   def setAllInSetEnabled(self, widgetSet, enabled):
       for widget in widgetSet:
           widget.setEnabled(enabled)

   def btnDrawAxesOnClick(self, checked):
       self.drawAxes = checked
       self.quickUpdate()

   def btnDrawGridOnClick(self, checked):
       self.drawGrid = checked
       self.quickUpdate()

   def btnCustTransformOnClick(self):
       matrixDialog = CustMatTransform.CustMatTransform()
       matrixDialog.show()
       result = matrixDialog.exec_()
       if result == Qw.QDialog.Accepted:
           objKey = self.currentlySelectedObj['selectedIndex']
           self.transformObject(objKey,
               matrixDialog.getTransformationMatrix(), not
               self.useGlobalCoords)

       # for now, unless we update the bouding box transformation.
       self.clearSelection()
       self.quickUpdate()

   def btnLoadEditorOnClick(self):
       pathToFile = os.path.splitext(self.fileName)[0]+'.asy'
       if self.fileChanged:
           save = "Save current file?"
           reply = Qw.QMessageBox.question(self, 'Message', save, Qw.QMessageBox.Yes,
                                           Qw.QMessageBox.No)
           if reply == Qw.QMessageBox.Yes:
               self.actionExport(pathToFile)

       subprocess.run(args=self.getExternalEditor(asypath=pathToFile));
       self.loadFile(pathToFile)

   def btnAddCodeOnClick(self):
       header = """
// xasy object created at $time
// Object Number: $uid
// This header is automatically generated by xasy.
// Your code here
"""
       header = string.Template(header).substitute(time=str(datetime.datetime.now()), uid=str(self.globalObjectCounter))

       with tempfile.TemporaryDirectory() as tmpdir:
           newPath = os.path.join(tmpdir, 'tmpcode.asy')
           f = io.open(newPath, 'w')
           f.write(header)
           f.close()

           subprocess.run(args=self.getExternalEditor(asypath=newPath))

           f = io.open(newPath, 'r')
           newItem = x2a.xasyScript(engine=self.asyEngine, canvas=self.xasyDrawObj)
           newItem.setScript(f.read())
           f.close()

       # newItem.replaceKey(str(self.globalObjectCounter) + ':')
       self.fileItems.append(newItem)
       self.addObjCreationUrs(newItem)
       self.asyfyCanvas()

       self.globalObjectCounter = self.globalObjectCounter + 1

   def softDeleteObj(self, objKey):
       maj, minor = objKey
       drawObj = self.drawObjects[maj][minor]
       item = drawObj.originalObj
       key = drawObj.key
       keyIndex = drawObj.keyIndex


       item.transfKeymap[key][keyIndex].deleted = True
       # item.asyfied = False

   def getSelectedObjInfo(self, objIndex):
       maj, minor = objIndex
       drawObj = self.drawObjects[maj][minor]
       item = drawObj.originalObj
       key = drawObj.key
       keyIndex = drawObj.keyIndex

       return item, key, keyIndex

   def transformObjKey(self, item, key, keyIndex, transform, applyFirst=False, drawObj=None):
       if isinstance(transform, np.ndarray):
           obj_transform = x2a.asyTransform.fromNumpyMatrix(transform)
       elif isinstance(transform, Qg.QTransform):
           assert transform.isAffine()
           obj_transform = x2a.asyTransform.fromQTransform(transform)
       else:
           obj_transform = transform

       scr_transform = obj_transform

       if not applyFirst:
           item.transfKeymap[key][keyIndex] = obj_transform * \
               item.transfKeymap[key][keyIndex]
           if drawObj is not None:
               drawObj.transform = scr_transform * drawObj.transform
       else:
           item.transfKeymap[key][keyIndex] = item.transfKeymap[key][keyIndex] * obj_transform
           if drawObj is not None:
               drawObj.transform = drawObj.transform * scr_transform

       if self.selectAsGroup:
           for (maj2, min2) in self.currentlySelectedObj['allSameKey']:
               if (maj2, min2) == (maj, minor):
                   continue
               obj = self.drawObjects[maj2][min2]
               newIndex = obj.keyIndex
               if not applyFirst:
                   item.transfKeymap[key][newIndex] = obj_transform * \
                       item.transfKeymap[key][newIndex]
                   obj.transform = scr_transform * obj.transform
               else:
                   item.transfKeymap[key][newIndex] = item.transfKeymap[key][newIndex] * obj_transform
                   obj.transform = obj.transform * scr_transform

       self.fileChanged = True
       self.quickUpdate()

   def transformObject(self, objKey, transform, applyFirst=False):
       maj, minor = objKey
       drawObj = self.drawObjects[maj][minor]
       item, key, keyIndex = self.getSelectedObjInfo(objKey)
       self.transformObjKey(item, key, keyIndex, transform, applyFirst, drawObj)

   def initializeEmptyFile(self):
       pass

   def getExternalEditor(self, **kwargs) -> str:
       editor = os.getenv("VISUAL")
       if(editor == None) :
           editor = os.getenv("EDITOR")
       if(editor == None) :
           rawExternalEditor = self.settings['externalEditor']
           rawExtEditorArgs = self.settings['externalEditorArgs']
       else:
           s = editor.split()
           rawExternalEditor = s[0]
           rawExtEditorArgs = s[1:]+["$asypath"]

       execEditor = [rawExternalEditor]

       for arg in rawExtEditorArgs:
           execEditor.append(string.Template(arg).substitute(**kwargs))

       return execEditor


   def loadFile(self, name):
       filename = os.path.abspath(name)
       if not os.path.isfile(filename):
           parts = os.path.splitext(filename)
           if parts[1] == '':
               filename = parts[0] + '.asy'

       if not os.path.isfile(filename):
           self.ui.statusbar.showMessage('File {0} not found'.format(filename))
           return

       self.ui.statusbar.showMessage('Load {0}'.format(filename))
       self.fileName = filename
       self.asyFileName = filename
       self.currDir = os.path.dirname(self.fileName)

       self.erase()

       f = open(self.fileName, 'rt')
       try:
           rawFileStr = f.read()
       except IOError:
           Qw.QMessageBox.critical(self, self.strings.fileOpenFailed, self.strings.fileOpenFailedText)
       else:
           rawText, transfDict = xf.extractTransformsFromFile(rawFileStr)
           item = x2a.xasyScript(canvas=self.xasyDrawObj, engine=self.asyEngine, transfKeyMap=transfDict)

           item.setScript(rawText)
           self.fileItems.append(item)
           self.asyfyCanvas(force=True)

           self.globalObjectCounter = item.maxKey+1
           self.asy2psmap = item.asy2psmap

       finally:
           f.close()
           self.btnPanCenterOnClick()

   def populateCanvasWithItems(self, forceUpdate=False):
       self.itemCount = 0
       for item in self.fileItems:
           self.drawObjects.append(item.generateDrawObjects(forceUpdate))

   def makePenCosmetic(self, painter):
       localPen = painter.pen()
       localPen.setCosmetic(True)
       painter.setPen(localPen)

   def copyItem(self):
       self.selectOnHover()
       if self.currentlySelectedObj['selectedIndex'] is not None:
           maj, minor = self.currentlySelectedObj['selectedIndex']
           if isinstance(self.fileItems[maj],x2a.xasyShape) or isinstance(self.fileItems[maj],x2a.xasyText):
               self.copiedObject = self.fileItems[maj].copy()
           else:
               self.ui.statusbar.showMessage('Copying not supported with current item type')
       else:
           self.ui.statusbar.showMessage('No object selected to copy')
           self.copiedObject = None
       self.clearSelection()

   def pasteItem(self):
       if hasattr(self, 'copiedObject') and not self.copiedObject is None:
           self.copiedObject = self.copiedObject.copy()
           self.addInPlace(self.copiedObject)
           mousePos = self.getWindowCoordinates() - self.copiedObject.path.toQPainterPath().boundingRect().center() - (Qc.QPointF(self.canvSize.width(), self.canvSize.height()) + Qc.QPointF(62, 201))/2 #I don't really know what that last constant is? Is it the size of the framing?
           newTransform = Qg.QTransform.fromTranslate(mousePos.x(), mousePos.y())
           self.currentlySelectedObj['selectedIndex'] = (self.globalObjectCounter - 1,0)
           self.currentlySelectedObj['key'],  self.currentlySelectedObj['allSameKey'] = self.selectObjectSet()
           newTransform = x2a.asyTransform.fromQTransform(newTransform)
           objKey = self.currentlySelectedObj['selectedIndex']
           self.addTransformationChanges(objKey, newTransform, not self.useGlobalCoords)
           self.transformObject(objKey, newTransform, not self.useGlobalCoords)
           self.quickUpdate()
       else:
           self.ui.statusbar.showMessage('No object to paste')

   def contextMenuEvent(self, event):
       #Note that we can't get anything from self.selectOnHover() here.
       try:
           self.contextWindowIndex = self.selectObject()[0] #for arrowifying
           maj = self.contextWindowIndex[0]
       except:
           return

       item=self.fileItems[maj]
       if item is not None and isinstance(item, x2a.xasyDrawnItem):
           self.contextWindowObject = item #For arrowifying
           self.contextWindow = ContextWindow.AnotherWindow(item,self)
           self.contextWindow.setMinimumWidth(420)
           #self.setCentralWidget(self.contextWindow) #I don't know what this does tbh.
           self.contextWindow.show()

   def focusInEvent(self,event):
       if self.mainCanvas.isActive():
           self.quickUpdate()

   def replaceObject(self,objectIndex,newObject):
       maj, minor = self.contextWindowIndex
       selectedObj = self.drawObjects[maj][minor]

       parent = selectedObj.parent()

       if isinstance(parent, x2a.xasyScript):
           objKey=(selectedObj.key, selectedObj.keyIndex)
           self.hiddenKeys.add(objKey)
           self.undoRedoStack.add(self.createAction(
               SoftDeletionChanges(selectedObj.parent(), objKey)
               ))
           self.softDeleteObj((maj, minor))
       else:
           index = self.fileItems.index(selectedObj.parent())

           self.undoRedoStack.add(self.createAction(
               HardDeletionChanges(selectedObj.parent(), index)
           ))

           self.fileItems.remove(selectedObj.parent())

       self.fileItems.append(newObject)
       self.drawObjects.append(newObject.generateDrawObjects(True)) #THIS DOES WORK, IT'S JUST REGENERATING THE SHAPE.

       self.checkUndoRedoButtons()
       self.fileChanged = True

       self.clearSelection()
       #self.asyfyCanvas()
       #self.quickUpdate()

   def terminateContextWindow(self):
       if self.contextWindow is not None:
           self.contextWindow.close()
       self.asyfyCanvas()
       self.quickUpdate()