#!/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()