diff options
Diffstat (limited to 'Build/source/utils/asymptote/GUI/Window1.py')
-rw-r--r-- | Build/source/utils/asymptote/GUI/Window1.py | 837 |
1 files changed, 672 insertions, 165 deletions
diff --git a/Build/source/utils/asymptote/GUI/Window1.py b/Build/source/utils/asymptote/GUI/Window1.py index 1f7b67b1fa1..134d3cd1968 100644 --- a/Build/source/utils/asymptote/GUI/Window1.py +++ b/Build/source/utils/asymptote/GUI/Window1.py @@ -18,6 +18,7 @@ import tempfile import datetime import string import atexit +import pickle import xasyUtils as xu import xasy2asy as x2a @@ -50,7 +51,6 @@ class TransformationChanges(ActionChanges): self.transformation = transformation self.isLocal = isLocal - class ObjCreationChanges(ActionChanges): def __init__(self, obj): self.object = obj @@ -60,6 +60,18 @@ class HardDeletionChanges(ActionChanges): 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 @@ -68,7 +80,7 @@ class AnchorMode: bottomRight = 4 bottomLeft = 5 customAnchor = 6 - + class GridMode: cartesian = 0 @@ -84,6 +96,14 @@ class SelectionMode: 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 @@ -92,14 +112,15 @@ class AddObjectMode: class MainWindow1(Qw.QMainWindow): defaultFrameStyle = """ - QFrame{{ + QFrame{{ padding: 4.0; - border-radius: 3.0; + border-radius: 3.0; background: rgb({0}, {1}, {2}) }} """ def __init__(self): + self.testingActions = [] super().__init__() self.ui = Ui_MainWindow() global devicePixelRatio @@ -109,12 +130,13 @@ class MainWindow1(Qw.QMainWindow): 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.identity() + self.asy2psmap = x2a.yflip() if self.settings['asyBaseLocation'] is not None: os.environ['ASYMPTOTE_DIR'] = self.settings['asyBaseLocation'] @@ -134,7 +156,8 @@ class MainWindow1(Qw.QMainWindow): # For initialization purposes self.canvSize = Qc.QSize() - self.filename = None + self.fileName = None + self.asyFileName = None self.currDir = None self.mainCanvas = None self.dpi = 300 @@ -165,9 +188,9 @@ class MainWindow1(Qw.QMainWindow): self.inMidTransformation = False self.addMode = None self.currentlySelectedObj = {'key': None, 'allSameKey': set(), 'selectedIndex': None, 'keyIndex': None} - self.pendingSelectedObjList = [] + self.pendingSelectedObjList = [] self.pendingSelectedObjIndex = -1 - + self.savedMousePosition = None self.currentBoundingBox = None self.selectionDelta = None @@ -177,9 +200,12 @@ class MainWindow1(Qw.QMainWindow): 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 @@ -211,8 +237,10 @@ class MainWindow1(Qw.QMainWindow): 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.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, @@ -225,17 +253,17 @@ class MainWindow1(Qw.QMainWindow): self.currAddOptionsWgt = None self.currAddOptions = { - 'options': self.settings, + 'options': self.settings, 'inscribed': True, 'sides': 3, 'centermode': True, - 'fontSize': None, + 'fontSize': None, 'asyengine': self.asyEngine, 'fill': self.ui.btnFill.isChecked(), 'closedPath': False, - 'useBezier': True, + 'useBezier': True, 'magnification': self.magnification, - 'editBezierlockMode': xbi.Web.LockMode.angleLock, + 'editBezierlockMode': xbi.Web.LockMode.angleLock, 'autoRecompute': False } @@ -249,7 +277,7 @@ class MainWindow1(Qw.QMainWindow): # commands switchboard self.commandsFunc = { - 'quit': Qc.QCoreApplication.quit, + 'quit': self.btnCloseFileonClick, 'undo': self.btnUndoOnClick, 'redo': self.btnRedoOnClick, 'manual': self.actionManual, @@ -261,10 +289,10 @@ class MainWindow1(Qw.QMainWindow): 'commandPalette': self.enterCustomCommand, 'clearGuide': self.clearGuides, 'finalizeAddObj': self.finalizeAddObj, - 'finalizeCurve': self.finalizeCurve, - 'finalizeCurveClosed': self.finalizeCurveClosed, + 'finalizeCurve': self.finalizeCurve, + 'finalizeCurveClosed': self.finalizeCurveClosed, 'setMag': self.setMagPrompt, - 'deleteObject': self.btnSelectiveDeleteOnClick, + 'deleteObject': self.btnSelectiveDeleteOnClick, 'anchorMode': self.switchToAnchorMode, 'moveUp': lambda: self.translate(0, -1), 'moveDown': lambda: self.translate(0, 1), @@ -274,10 +302,17 @@ class MainWindow1(Qw.QMainWindow): '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), + '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, - 'zoomIn': lambda: self.arrowButtons(0, 1, False, True), - 'zoomOut': lambda: self.arrowButtons(0, -1, False, True) + 'copy': self.copyItem, + 'paste': self.pasteItem } self.hiddenKeys = set() @@ -291,6 +326,7 @@ class MainWindow1(Qw.QMainWindow): # from xasyoptions config file self.loadKeyMaps() self.setupXasyOptions() + self.populateOpenRecent() self.colorDialog = Qw.QColorDialog(x2a.asyPen.convertToQColor(self._currentPen.color), self) self.initPenInterface() @@ -320,7 +356,7 @@ class MainWindow1(Qw.QMainWindow): def getScrsTransform(self): # pipeline: # assuming origin <==> top left - # (Pan) * (Translate) * (Flip the images) * (Zoom) * (Obj transform) * (Base Information) + # (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) @@ -366,6 +402,12 @@ class MainWindow1(Qw.QMainWindow): 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: @@ -373,6 +415,11 @@ class MainWindow1(Qw.QMainWindow): 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 @@ -381,7 +428,10 @@ class MainWindow1(Qw.QMainWindow): def handleArguments(self): if self.args.filename is not None: - self.loadFile(self.args.filename) + if os.path.exists(self.args.filename): + self.actionOpen(os.path.abspath(self.args.filename)) + else: + self.loadFile(self.args.filename) else: self.initializeEmptyFile() @@ -417,14 +467,18 @@ class MainWindow1(Qw.QMainWindow): 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.btnExportAsyOnClick) + self.ui.actionExportAsymptote.triggered.connect(self.btnExportAsymptoteOnClick) + self.ui.actionExportToAsy.triggered.connect(self.btnExportToAsyOnClick) def setupXasyOptions(self): if self.settings['debugMode']: @@ -443,7 +497,7 @@ class MainWindow1(Qw.QMainWindow): self.ui.btnSave.clicked.connect(self.btnSaveonClick) self.ui.btnQuickScreenshot.clicked.connect(self.btnQuickScreenshotOnClick) - # self.ui.btnExportAsy.clicked.connect(self.btnExportAsyOnClick) + # self.ui.btnExportAsy.clicked.connect(self.btnExportAsymptoteOnClick) self.ui.btnDrawAxes.clicked.connect(self.btnDrawAxesOnClick) # self.ui.btnAsyfy.clicked.connect(lambda: self.asyfyCanvas(True)) @@ -467,7 +521,7 @@ class MainWindow1(Qw.QMainWindow): self.ui.btnAnchor.clicked.connect(self.btnAnchorModeOnClick) self.ui.btnSelectColor.clicked.connect(self.btnColorSelectOnClick) - self.ui.txtLineWidth.textEdited.connect(self.txtLineWithEdited) + self.ui.txtLineWidth.textEdited.connect(self.txtLineWidthEdited) # self.ui.btnCreateCurve.clicked.connect(self.btnCreateCurveOnClick) self.ui.btnDrawGrid.clicked.connect(self.btnDrawGridOnClick) @@ -475,6 +529,7 @@ class MainWindow1(Qw.QMainWindow): 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) @@ -489,16 +544,19 @@ class MainWindow1(Qw.QMainWindow): 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): - self.currentModeStack = [SelectionMode.delete] - self.ui.statusbar.showMessage('Delete Mode') - self.clearSelection() - self.updateChecks() + 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: @@ -507,18 +565,25 @@ class MainWindow1(Qw.QMainWindow): else: pass # TODO: How to handle this case? - # Like AutoCAD? + # Like AutoCAD? self.ui.txtTerminalPrompt.clear() - def btnFillOnClick(self, checked): + def btnFillOnClick(self, checked): + if self.currentModeStack == [SelectionMode.selectEdit]: + if isinstance(self.addMode,xbi.InteractiveBezierEditor): + self.addMode.swapObjFill() #Check for crashes self.currAddOptions['fill'] = checked self.ui.btnOpenCurve.setEnabled(not checked) self.ui.btnOpenPoly.setEnabled(not checked) def btnSelectEditOnClick(self): - self.ui.statusbar.showMessage('Edit mode') - self.currentModeStack = [SelectionMode.selectEdit] - self.updateChecks() + 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): @@ -544,7 +609,7 @@ class MainWindow1(Qw.QMainWindow): except Exception: pass - self.currentModeStack[-1] = None + #self.currentModeStack[-1] = None self.addMode.objectCreated.connect(self.addInPlace) self.updateModeBtnsOnly() @@ -557,7 +622,10 @@ class MainWindow1(Qw.QMainWindow): def addInPlace(self, obj): obj.asyengine = self.asyEngine - obj.pen = self.currentPen + 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 @@ -576,11 +644,6 @@ class MainWindow1(Qw.QMainWindow): self.currentGuides.clear() self.quickUpdate() - def btnAddCircleOnClick(self): - self.addMode = InplaceAddObj.AddCircle(self) - self.ui.statusbar.showMessage('') - self.updateOptionWidget() - 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.' @@ -598,49 +661,90 @@ class MainWindow1(Qw.QMainWindow): self.ui.statusbar.showMessage(self.Hint+self.HintClose) def btnAddBezierInplaceOnClick(self): + self.fileChanged = True self.addMode = InplaceAddObj.AddBezierShape(self) self.updateOptionWidget() def btnAddOpenLineOnClick(self): - self.currAddOptions['useBezier'] = False - self.currAddOptions['closedPath'] = False - self.drawHintOpen() - self.btnAddBezierInplaceOnClick() + 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): - self.currAddOptions['useBezier'] = False - self.currAddOptions['closedPath'] = True - self.drawHint() - self.btnAddBezierInplaceOnClick() + 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): - self.currAddOptions['useBezier'] = True - self.currAddOptions['closedPath'] = False - self.drawHintOpen() - self.btnAddBezierInplaceOnClick() + 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): - self.currAddOptions['useBezier'] = True - self.currAddOptions['closedPath'] = True - self.drawHint() - self.btnAddBezierInplaceOnClick() + 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): - self.addMode = InplaceAddObj.AddPoly(self) - self.ui.statusbar.showMessage('') - self.updateOptionWidget() + 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 btnAddLabelOnClick(self): - self.addMode = InplaceAddObj.AddLabel(self) - self.ui.statusbar.showMessage('') - self.updateOptionWidget() + 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 updateCurve(self, valid, newCurve): - self.previewCurve = newCurve - self.quickUpdate() + 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, + self.undoRedoStack.add(self.createAction(TransformationChanges(objIndex, transform, isLocal))) self.checkUndoRedoButtons() @@ -665,7 +769,11 @@ class MainWindow1(Qw.QMainWindow): parent = selectedObj.parent() if isinstance(parent, x2a.xasyScript): - self.hiddenKeys.add((selectedObj.key, selectedObj.keyIndex)) + 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()) @@ -673,10 +781,10 @@ class MainWindow1(Qw.QMainWindow): self.undoRedoStack.add(self.createAction( HardDeletionChanges(selectedObj.parent(), index) )) - self.checkUndoRedoButtons() - + self.fileItems.remove(selectedObj.parent()) + self.checkUndoRedoButtons() self.fileChanged = True self.clearSelection() self.asyfyCanvas() @@ -709,8 +817,16 @@ class MainWindow1(Qw.QMainWindow): def btnUndoOnClick(self): - self.undoRedoStack.undo() - self.checkUndoRedoButtons() + 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() @@ -731,6 +847,12 @@ class MainWindow1(Qw.QMainWindow): 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): @@ -742,6 +864,12 @@ class MainWindow1(Qw.QMainWindow): 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? @@ -765,11 +893,26 @@ class MainWindow1(Qw.QMainWindow): if result: self.execCustomCommand(commandText) - def addItemFromPath(self, path): - newItem = x2a.xasyShape(path, self.asyEngine, pen=self.currentPen) + def addXasyShapeFromPath(self, path, pen = None, transform = x2a.identity(), key = None): + if not pen: + pen = self.currentPen + else: + pen = x2a.asyPen(self.asyEngine, color = pen['color'], width = pen['width'], pen_options = pen['options']) + + newItem = x2a.xasyShape(path, self.asyEngine, pen = pen, transform = transform) + newItem.setKey(key) self.fileItems.append(newItem) - self.fileChanged = True - self.asyfyCanvas() + + 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' @@ -778,11 +921,35 @@ class MainWindow1(Qw.QMainWindow): def actionAbout(self): Qw.QMessageBox.about(self,"xasy","This is xasy "+xasyVersion.xasyVersion+"; a graphical front end to the Asymptote vector graphics language: https://asymptote.sourceforge.io/") - def btnExportAsyOnClick(self): + 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 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'] @@ -805,14 +972,14 @@ class MainWindow1(Qw.QMainWindow): } } - formats = ['pdf', 'svg', 'eps', 'png', '*'] + 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] + '.pdf' + rawFile = os.path.splitext(os.path.basename(self.fileName))[0] + '.asy' diag.selectFile(rawFile) diag.setNameFilter(formatText) @@ -823,25 +990,107 @@ class MainWindow1(Qw.QMainWindow): return finalFiles = diag.selectedFiles() - - with io.StringIO() as finalCode: - xf.saveFile(finalCode, self.fileItems, self.asy2psmap) - finalString = finalCode.getvalue() + finalString = xf.xasy2asyCode(self.fileItems, self.asy2psmap) for file in finalFiles: ext = os.path.splitext(file) if len(ext) < 2: - ext = 'pdf' + ext = 'asy' else: ext = ext[1][1:] + if ext == 'asy': + pathToFile = os.path.splitext(file)[0]+'.'+ext + asyFile = io.open(os.path.realpath(pathToFile), 'w') + xf.saveFile(asyFile, self.fileItems, self.asy2psmap) + asyFile.close() + 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) - with subprocess.Popen(args=[self.asyPath, '-f{0}'.format(ext), '-o{0}'.format(file), '-'], encoding='utf-8', - stdin=subprocess.PIPE) as asy: - print('test:', finalString) - asy.stdin.write(finalString) - asy.stdin.close() - asy.wait(timeout=35) + 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(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']) + else: + print("ERROR") + + self.asy2psmap = x2a.asyTransform(xasyObjects['asy2psmap']) + 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 @@ -865,28 +1114,147 @@ class MainWindow1(Qw.QMainWindow): def erase(self): self.fileItems.clear() + self.hiddenKeys.clear() + self.undoRedoStack.clear() + self.checkUndoRedoButtons() self.fileChanged = False - def actionOpen(self): + #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: - save="Save current file?" - reply=Qw.QMessageBox.question(self,'Message',save,Qw.QMessageBox.Yes, - Qw.QMessageBox.No) + 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 - filename = Qw.QFileDialog.getOpenFileName(self, 'Open Asymptote File','', '*.asy') - if filename[0]: - self.loadFile(filename[0]) + 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: + if self.fileName is None: self.actionSaveAs() + else: - saveFile = io.open(self.filename, 'w') - xf.saveFile(saveFile, self.fileItems, self.asy2psmap) - saveFile.close() + _, 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: @@ -895,15 +1263,30 @@ class MainWindow1(Qw.QMainWindow): 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): - saveLocation = Qw.QFileDialog.getSaveFileName(self, 'Save File')[0] + initSave = os.path.splitext(str(self.fileName))[0]+'.xasy' + saveLocation = Qw.QFileDialog.getSaveFileName(self, 'Save File', initSave, "Xasy File (*.xasy)")[0] if saveLocation: - saveFile = io.open(saveLocation, 'w') - xf.saveFile(saveFile, self.fileItems, self.asy2psmap) - saveFile.close() - self.filename = 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','') @@ -913,6 +1296,9 @@ class MainWindow1(Qw.QMainWindow): def btnLoadFileonClick(self): self.actionOpen() + def btnCloseFileonClick(self): + self.actionClose() + def btnSaveonClick(self): self.actionSave() @@ -933,7 +1319,7 @@ class MainWindow1(Qw.QMainWindow): self._currentPen.setColorFromQColor(self.colorDialog.selectedColor()) self.updateFrameDispColor() - def txtLineWithEdited(self, text): + def txtLineWidthEdited(self, text): new_val = xu.tryParse(text, float) if new_val is not None: if new_val > 0: @@ -985,12 +1371,12 @@ class MainWindow1(Qw.QMainWindow): def mouseMoveEvent(self, mouseEvent: Qg.QMouseEvent): # TODO: Actually refine grid snapping... if not self.ui.imgLabel.underMouse() and not self.mouseDown: - return + return self.updateMouseCoordLabel() asyPos, canvasPos = self.getAsyCoordinates() - # add mode + # add mode if self.addMode is not None: if self.addMode.active: self.addMode.mouseMove(asyPos, mouseEvent) @@ -1016,7 +1402,7 @@ class MainWindow1(Qw.QMainWindow): self.quickUpdate() return - # otherwise, in transformation + # otherwise, in transformation if self.inMidTransformation: if self.currentModeStack[-1] == SelectionMode.translate: newPos = canvasPos - self.savedMousePosition @@ -1080,11 +1466,11 @@ class MainWindow1(Qw.QMainWindow): self.pendingSelectedObjList.clear() self.pendingSelectedObjIndex = -1 self.quickUpdate() - return + return def mouseReleaseEvent(self, mouseEvent): - assert isinstance(mouseEvent, Qg.QMouseEvent) + assert isinstance(mouseEvent, Qg.QMouseEvent) if not self.mouseDown: return @@ -1124,8 +1510,7 @@ class MainWindow1(Qw.QMainWindow): keyModifiers = keyModifiers | defaultModifiers if keyModifiers & int(Qc.Qt.ControlModifier): oldMag = self.magnification - - factor=0.5/devicePixelRatio; + factor = 0.5/devicePixelRatio cx, cy = self.canvSize.width()*factor, self.canvSize.height()*factor centerPoint = Qc.QPointF(cx, cy) * self.getScrsTransform().inverted()[0] @@ -1203,7 +1588,7 @@ class MainWindow1(Qw.QMainWindow): 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 @@ -1252,7 +1637,15 @@ class MainWindow1(Qw.QMainWindow): self.addMode = None self.deleteAddOptions() - def editFinalized(self): + 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 @@ -1260,7 +1653,10 @@ class MainWindow1(Qw.QMainWindow): def editRejected(self): self.addMode.resetObj() - self.editFinalized() + 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""" @@ -1270,12 +1666,20 @@ class MainWindow1(Qw.QMainWindow): # bezier path self.addMode = xbi.InteractiveBezierEditor(self, obj, self.currAddOptions) self.addMode.objectUpdated.connect(self.objectUpdated) - self.addMode.editAccepted.connect(self.editFinalized) + 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() @@ -1359,7 +1763,7 @@ class MainWindow1(Qw.QMainWindow): # but it's much more work... newCenter = self.magnification * newCenter self.panOffset = [-newCenter.x(), newCenter.y()] - + self.quickUpdate() def selectObject(self): @@ -1410,7 +1814,7 @@ class MainWindow1(Qw.QMainWindow): def getWindowCoordinates(self): # assert self.ui.imgLabel.underMouse() return self.mapFromGlobal(Qg.QCursor.pos()) - + def refreshCanvas(self): if self.mainCanvas.isActive(): self.mainCanvas.end() @@ -1430,16 +1834,20 @@ class MainWindow1(Qw.QMainWindow): 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) + 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 @@ -1466,7 +1874,7 @@ class MainWindow1(Qw.QMainWindow): if self.pendingSelectedObjList: maj, minor = self.pendingSelectedObjList[self.pendingSelectedObjIndex] self.drawObjects[maj][minor].draw(canvas=self.mainCanvas, dpi=dpi) - # and apply the preview too... + # and apply the preview too... elif activeItem is not None: if self.useGlobalCoords: activeItem.draw(self.newTransform, canvas=self.mainCanvas, dpi=dpi) @@ -1474,6 +1882,17 @@ class MainWindow1(Qw.QMainWindow): 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) @@ -1502,6 +1921,7 @@ class MainWindow1(Qw.QMainWindow): 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 = x + ((xMinor / (minorGridCount + 1)) * majorGrid) preCanvas.drawLine(Qc.QLine(xCoord, -9999, xCoord, 9999)) @@ -1509,17 +1929,20 @@ class MainWindow1(Qw.QMainWindow): 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 = 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) preCanvas.drawLine(Qc.QLine(-9999, y, 9999, y)) preCanvas.drawLine(Qc.QLine(-9999, -y, 9999, -y)) for x in np.arange(0, 2 * x_range + 1, majorGrid): preCanvas.setPen(majorGridCol) + self.makePenCosmetic(preCanvas) preCanvas.drawLine(Qc.QLine(x, -9999, x, 9999)) preCanvas.drawLine(Qc.QLine(-x, -9999, -x, 9999)) @@ -1568,6 +1991,7 @@ class MainWindow1(Qw.QMainWindow): 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)) @@ -1590,13 +2014,14 @@ class MainWindow1(Qw.QMainWindow): 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( @@ -1621,13 +2046,14 @@ class MainWindow1(Qw.QMainWindow): 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: @@ -1657,11 +2083,29 @@ class MainWindow1(Qw.QMainWindow): 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) @@ -1670,22 +2114,36 @@ class MainWindow1(Qw.QMainWindow): 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): - self.lockY = checked - if self.lockX: - self.lockX = False - self.ui.btnAlignY.setChecked(False) + 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): - self.lockX = checked - if self.lockY: - self.lockY = False - self.ui.btnAlignX.setChecked(False) + 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: @@ -1704,22 +2162,31 @@ class MainWindow1(Qw.QMainWindow): self.updateChecks() def btnRotateOnClick(self): - self.currentModeStack = [SelectionMode.rotate] - self.ui.statusbar.showMessage('Rotate mode') - self.clearSelection() - self.updateChecks() + 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): - self.currentModeStack = [SelectionMode.scale] - self.ui.statusbar.showMessage('Scale mode') - self.clearSelection() - self.updateChecks() + 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): - self.currentModeStack = [SelectionMode.pan] - self.ui.statusbar.showMessage('Pan mode') - self.clearSelection() - self.updateChecks() + 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 @@ -1754,24 +2221,26 @@ class MainWindow1(Qw.QMainWindow): 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.actionSave() - - subprocess.Popen(args=self.getExternalEditor(asypath=self.filename)); + self.actionExport(pathToFile) + + subprocess.run(args=self.getExternalEditor(asypath=pathToFile)); + self.loadFile(pathToFile) def btnAddCodeOnClick(self): header = """ -// xasy object created at $time +// xasy object created at $time // Object Number: $uid -// This header is automatically generated by xasy. +// 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') @@ -1791,6 +2260,7 @@ class MainWindow1(Qw.QMainWindow): self.asyfyCanvas() self.globalObjectCounter = self.globalObjectCounter + 1 + def softDeleteObj(self, objKey): maj, minor = objKey drawObj = self.drawObjects[maj][minor] @@ -1869,7 +2339,7 @@ class MainWindow1(Qw.QMainWindow): s = editor.split() rawExternalEditor = s[0] rawExtEditorArgs = s[1:]+["$asypath"] - + execEditor = [rawExternalEditor] for arg in rawExtEditorArgs: @@ -1881,34 +2351,37 @@ class MainWindow1(Qw.QMainWindow): def loadFile(self, name): filename = os.path.abspath(name) if not os.path.isfile(filename): - filename = filename + '.asy' + 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.currDir = os.path.dirname(self.filename) + self.fileName = filename + self.asyFileName = filename + self.currDir = os.path.dirname(self.fileName) self.erase() - f = open(self.filename, 'rt') + f = open(self.fileName, 'rt') try: rawFileStr = f.read() except IOError: Qw.QMessageBox.critical(self, self.strings.fileOpenFailed, self.strings.fileOpenFailedText) else: - rawText, transfDict, maxKey = xf.extractTransformsFromFile(rawFileStr) + 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(True) + self.asyfyCanvas(force=True) - maxKey2 = item.getMaxKeyCounter() + self.globalObjectCounter = item.maxKey+1 self.asy2psmap = item.asy2psmap - self.globalObjectCounter = max(maxKey + 1, maxKey2) + finally: f.close() @@ -1916,3 +2389,37 @@ class MainWindow1(Qw.QMainWindow): 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') |