From 86e2c9e904b8449aadf507ca3c7fa07a07215f0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Thu, 26 Sep 2019 14:06:11 +0200 Subject: [PATCH] Various bugfixes & usability improvements. 1) Correctly unmask cursor and dispatch delta patches on successful {re,un}do. 2) Don't prompt to save twice on exit via Exit {accelerator,menu item}. 3) Fix cursor artifacts by always resetting origin point on DC whilst unmasking cursor cells. 4) Fix {re,un}do {accelerator,{menu,toolbar} item} desynchronisation with actual canvas journal undo level. 5) Remove scattered remnants of initial implementation of unimplemented italic support. 6) Replace rendering transparent cursor w/ manual blending over wx.GraphicsContext() due to canvas bitmap masking & performance degradation. assets/text/TODO: updated. --- assets/text/TODO | 2 +- libcanvas/CanvasExportStore.py | 3 +- libcanvas/CanvasImportStore.py | 5 +- libgui/GuiCanvasColours.py | 2 +- libgui/GuiCanvasWxBackend.py | 144 +++++++++++++----------------- libroar/RoarCanvasCommandsEdit.py | 6 +- libroar/RoarCanvasCommandsFile.py | 10 ++- libroar/RoarCanvasWindow.py | 13 ++- libroar/RoarClient.py | 6 +- 9 files changed, 86 insertions(+), 105 deletions(-) diff --git a/assets/text/TODO b/assets/text/TODO index 6ef0fd4..a957828 100644 --- a/assets/text/TODO +++ b/assets/text/TODO @@ -1,4 +1,4 @@ -1) ANSI CSI CU[BDPU] sequences & italic +1) ANSI CSI CU[BDPU] sequences 2) Fix & finish Arabic/RTL text tool support 3) Documentation, instrumentation & unit tests 4) Layers & layout (e.g. for comics, zines, etc.) diff --git a/libcanvas/CanvasExportStore.py b/libcanvas/CanvasExportStore.py index 4c733f8..c653e9f 100644 --- a/libcanvas/CanvasExportStore.py +++ b/libcanvas/CanvasExportStore.py @@ -23,8 +23,7 @@ class CanvasExportStore(): class _CellState(): CS_NONE = 0x00 CS_BOLD = 0x01 - CS_ITALIC = 0x02 - CS_UNDERLINE = 0x04 + CS_UNDERLINE = 0x02 ImgurUploadUrl = "https://api.imgur.com/3/upload.json" PastebinPostUrl = "https://pastebin.com/api/api_post.php" diff --git a/libcanvas/CanvasImportStore.py b/libcanvas/CanvasImportStore.py index e36d7aa..7c57157 100644 --- a/libcanvas/CanvasImportStore.py +++ b/libcanvas/CanvasImportStore.py @@ -12,8 +12,7 @@ class CanvasImportStore(): class _CellState(): CS_NONE = 0x00 CS_BOLD = 0x01 - CS_ITALIC = 0x02 - CS_UNDERLINE = 0x04 + CS_UNDERLINE = 0x02 def _flipCellStateBit(self, bit, cellState): return cellState & ~bit if cellState & bit else cellState | bit @@ -108,7 +107,7 @@ class CanvasImportStore(): else: inCurColours = (15, -1); inCurCol += 1; elif inChar == "\u0006": - inCellState = self._flipCellStateBit(self._CellState.CS_ITALIC, inCellState); inCurCol += 1; + inCurCol += 1 elif inChar == "\u000f": inCellState |= self._CellState.CS_NONE; inCurColours = (15, -1); inCurCol += 1; elif inChar == "\u0016": diff --git a/libgui/GuiCanvasColours.py b/libgui/GuiCanvasColours.py index ff6d6b5..679d23f 100644 --- a/libgui/GuiCanvasColours.py +++ b/libgui/GuiCanvasColours.py @@ -5,7 +5,7 @@ # # -# Colours: mIRC colour number to RGBA map given none of ^[BFV_] (bold, italic, reverse, underline) +# Colours: mIRC colour number to RGBA map given none of ^[BV_] (bold, reverse, underline) # Colours = [ [255, 255, 255, 255, "White"], diff --git a/libgui/GuiCanvasWxBackend.py b/libgui/GuiCanvasWxBackend.py index 68b7551..b410493 100644 --- a/libgui/GuiCanvasWxBackend.py +++ b/libgui/GuiCanvasWxBackend.py @@ -67,62 +67,64 @@ class GuiCanvasWxBackend(): class _CellState(): CS_NONE = 0x00 CS_BOLD = 0x01 - CS_ITALIC = 0x02 - CS_UNDERLINE = 0x04 + CS_UNDERLINE = 0x02 - def _drawBrushPatch(self, eventDc, isCursor, patch, point): - absPoint = self._xlatePoint(point) - dc = self._setBrushPatchColours(eventDc, isCursor, patch) - dc.DrawRectangle(*absPoint, *self.cellSize) + def _blendColours(self, bg, fg): + return [int((fg * 0.75) + (bg * (1.0 - 0.75))) for bg, fg in zip(Colours[bg][:3], Colours[fg][:3])] - def _drawCharPatch(self, eventDc, isCursor, patch, point): - absPoint = self._xlatePoint(point) - dc, pen = self._setCharPatchColours(eventDc, isCursor, patch) - dc.DrawRectangle(*absPoint, *self.cellSize) - if (patch[2] & self._CellState.CS_UNDERLINE) or (patch[3] == "_"): - dc.SetPen(self._pens[patch[0]]); - if not isCursor: - dc.DrawLine(absPoint[0], absPoint[1] + self.cellSize[1] - 1, absPoint[0] + self.cellSize[0], absPoint[1] + self.cellSize[1] - 1) - else: - dc.DrawLines((wx.Point2D(absPoint[0], absPoint[1] + self.cellSize[1] - 1), wx.Point2D(absPoint[0] + self.cellSize[0], absPoint[1] + self.cellSize[1] - 1),)) - dc.SetPen(pen) - if patch[3] != "_": - if not isCursor: - oldClippingRegion = dc.GetClippingBox() - dc.DestroyClippingRegion(); dc.SetClippingRegion(*absPoint, *self.cellSize); - dc.SetTextBackground(wx.Colour(Colours[patch[1]][:4])); dc.SetTextForeground(wx.Colour(Colours[patch[0]][:4])); - else: - dc.ResetClip(); dc.Clip(wx.Region(*absPoint, *self.cellSize)); - dc.DrawText(patch[3], *absPoint) - if not isCursor: - dc.DestroyClippingRegion() + def _blendColoursBrush(self, bg, fg): + colour = self._blendColours(bg, fg) + return wx.Brush(wx.Colour(colour), wx.BRUSHSTYLE_SOLID), wx.Pen(wx.Colour(colour), 1) + + def _drawPatch(self, eventDc, isCursor, patch, patchBg, point): + absPoint, charFlag = self._xlatePoint(point), False + if (patch[3] == " ") and (patch[1] == -1): + charFlag, patch = True, [*patch[:-1], "░"] + textBg, textFg = wx.Colour(Colours[patch[1]][:4]), wx.Colour(Colours[patch[0]][:4]) + if isCursor and (patch[3] == " ") and ((patchBg[3] != " ") or (patchBg[2] & self._CellState.CS_UNDERLINE)): + charFlag, patch = True, [*patch[:-2], *patchBg[2:]] + textFg = wx.Colour(self._blendColours(patchBg[0], patch[1])) + elif (patch[3] != " ") or (patch[2] & self._CellState.CS_UNDERLINE): + charFlag = True + textBg, textFg = wx.Colour(Colours[patch[1]][:4]), wx.Colour(Colours[patch[0]][:4]) + brush, pen = self._setBrushColours(eventDc, isCursor, patch, patchBg) + eventDc.DrawRectangle(*absPoint, *self.cellSize) + if charFlag: + if (patch[2] & self._CellState.CS_UNDERLINE) or (patch[3] == "_"): + eventDc.SetPen(self._pens[patch[0]]); + eventDc.DrawLine(absPoint[0], absPoint[1] + self.cellSize[1] - 1, absPoint[0] + self.cellSize[0], absPoint[1] + self.cellSize[1] - 1) + eventDc.SetPen(pen) + if patch[3] != "_": + oldClippingRegion = eventDc.GetClippingBox() + eventDc.SetFont(self._font) + eventDc.DestroyClippingRegion(); eventDc.SetClippingRegion(*absPoint, *self.cellSize); + eventDc.SetTextForeground(textFg) + eventDc.DrawText(patch[3], *absPoint) + eventDc.DestroyClippingRegion() + if isCursor: + brush.Destroy(); pen.Destroy(); + if self._lastBrush != None: + eventDc.SetBrush(self._lastBrush) + if self._lastPen != None: + eventDc.SetPen(self._lastPen) def _finiBrushesAndPens(self): [brush.Destroy() for brush in self._brushes or []] - [brushTransp.Destroy() for brushTransp in self._brushesTransp or []] [pen.Destroy() for pen in self._pens or []] - [penTransp.Destroy() for penTransp in self._pensTransp or []] self._brushAlpha.Destroy(); self._penAlpha.Destroy(); - self._brushAlphaTransp.Destroy(); self._penAlphaTransp.Destroy(); self._brushes, self._lastBrush, self._lastPen, self._pens = None, None, None, None - self._brushesTransp, self._lastBrushTransp, self._lastPenTransp, self._pensTransp = None, None, None, None def _initBrushesAndPens(self): self._brushes, self._pens = [None for x in range(len(Colours))], [None for x in range(len(Colours))] - self._brushesTransp, self._pensTransp = [None for x in range(len(Colours))], [None for x in range(len(Colours))] for mircColour in range(len(Colours)): self._brushes[mircColour] = wx.Brush(wx.Colour(Colours[mircColour][:4]), wx.BRUSHSTYLE_SOLID) - self._brushesTransp[mircColour] = wx.Brush(wx.Colour(*Colours[mircColour][:3], 200), wx.BRUSHSTYLE_SOLID) self._pens[mircColour] = wx.Pen(wx.Colour(Colours[mircColour][:4]), 1) - self._pensTransp[mircColour] = wx.Pen(wx.Colour(*Colours[mircColour][:3], 200), 1, wx.PENSTYLE_TRANSPARENT) self._brushAlpha = wx.Brush(wx.Colour(Colours[14][:4]), wx.BRUSHSTYLE_SOLID) - self._brushAlphaTransp = wx.Brush(wx.Colour(*Colours[14][:3], 200), wx.BRUSHSTYLE_SOLID) self._penAlpha = wx.Pen(wx.Colour(Colours[14][:4]), 1) - self._penAlphaTransp = wx.Pen(wx.Colour(*Colours[14][:3], 200), 1, wx.PENSTYLE_TRANSPARENT) self._lastBrush, self._lastPen = None, None - self._lastBrushTransp, self._lastPenTransp = None, None def _reshapeArabic(self, canvas, eventDc, isCursor, patch, point): + patches = [] lastCell = point[0] while True: if ((lastCell + 1) >= (canvas.size[0] - 1)) \ @@ -147,76 +149,52 @@ class GuiCanvasWxBackend(): runCell[3] = self.arabicShapes[runCell[3]][1]; connect = True; else: runCell[3] = self.arabicShapes[runCell[3]][0]; connect = False; - self._drawCharPatch(eventDc, isCursor, runCell, [runX, point[1]]) + patches += [[runX, point[1], *runCell]] runCell = list(patch[2:]) if connect and (self.arabicShapes[patch[5]][3] != None): runCell[3] = self.arabicShapes[patch[5]][3] else: runCell[3] = self.arabicShapes[patch[5]][0] - self._drawCharPatch(eventDc, isCursor, runCell, [point[0], point[1]]) + patches += [[*point, *runCell]] + return patches - def _setBrushPatchColours(self, dc, isCursor, patch): - if not isCursor: - brushAlpha, brushes, dc_, penAlpha, pens = self._brushAlpha, self._brushes, dc, self._penAlpha, self._pens - else: - brushAlpha, brushes, dc_, penAlpha, pens = self._brushAlphaTransp, self._brushesTransp, wx.GraphicsContext.Create(dc), self._penAlphaTransp, self._pensTransp + def _setBrushColours(self, dc, isCursor, patch, patchBg): if ((patch[0] != -1) and (patch[1] != -1)) \ or ((patch[0] == -1) and (patch[1] != -1)): - brush, pen = brushes[patch[1]], pens[patch[1]] + if not isCursor: + brush, pen = self._brushes[patch[1]], self._pens[patch[1]] + else: + brush, pen = self._blendColoursBrush(patchBg[1], patch[1]) else: - brush, pen = brushAlpha, penAlpha + if not isCursor: + brush, pen = self._brushAlpha, self._penAlpha + else: + brush, pen = self._blendColoursBrush(patchBg[1], 14) if not isCursor: if self._lastBrush != brush: - dc_.SetBrush(brush); self._lastBrush = brush; + dc.SetBrush(brush); self._lastBrush = brush; if self._lastPen != pen: - dc_.SetPen(pen); self._lastPen = pen; + dc.SetPen(pen); self._lastPen = pen; else: - dc_.SetBrush(brush); dc_.SetPen(pen); - return dc_ - - def _setCharPatchColours(self, dc, isCursor, patch): - if not isCursor: - brushAlpha, brushes, dc_, penAlpha, pens = self._brushAlpha, self._brushes, dc, self._penAlpha, self._pens - dc_.SetFont(self._font) - else: - brushAlpha, brushes, dc_, penAlpha, pens = self._brushAlphaTransp, self._brushesTransp, wx.GraphicsContext.Create(dc), self._penAlphaTransp, self._pensTransp - if (patch[0] != -1) and (patch[1] != -1): - brush, fontColour, pen = brushes[patch[1]], Colours[patch[0]][:3], pens[patch[1]] - elif (patch[0] == -1) and (patch[1] == -1): - brush, fontColour, pen = brushAlpha, Colours[14][:4], penAlpha - elif patch[0] == -1: - brush, fontColour, pen = brushes[patch[1]], Colours[14][:3], pens[patch[1]] - elif patch[1] == -1: - brush, fontColour, pen = brushAlpha, Colours[patch[0]][:3], penAlpha - if not isCursor: - if self._lastBrush != brush: - dc_.SetBrush(brush); self._lastBrush = brush; - if self._lastPen != pen: - dc_.SetPen(pen); self._lastPen = pen; - else: - dc_.SetBrush(brush); dc_.SetFont(self._font, wx.Colour(fontColour)); dc_.SetPen(pen); - return dc_, pen + dc.SetBrush(brush); dc.SetPen(pen); + return brush, pen def _xlatePoint(self, point): return [a * b for a, b in zip(point, self.cellSize)] def drawCursorMaskWithJournal(self, canvas, canvasJournal, eventDc): + eventDcOrigin = eventDc.GetDeviceOrigin(); eventDc.SetDeviceOrigin(0, 0); [self.drawPatch(canvas, eventDc, patch) for patch in canvasJournal.popCursor()] + eventDc.SetDeviceOrigin(*eventDcOrigin) def drawPatch(self, canvas, eventDc, patch, isCursor=False): point = patch[:2] if [(c >= 0) and (c < s) for c, s in zip(point, self.canvasSize)] == [True, True]: - if patch[5] == " ": - if patch[3] == -1: - self._drawCharPatch(eventDc, isCursor, [*patch[2:-1], "░"], point) - elif patch[4] & self._CellState.CS_UNDERLINE: - self._drawCharPatch(eventDc, isCursor, patch[2:], point) - else: - self._drawBrushPatch(eventDc, isCursor, patch[2:], point) - elif patch[5] in self.arabicShapes: - self._reshapeArabic(canvas, eventDc, isCursor, patch, point) + if patch[5] in self.arabicShapes: + for patchReshaped in self._reshapeArabic(canvas, eventDc, isCursor, patch, point): + self._drawPatch(eventDc, isCursor, patchReshaped[2:], canvas.map[patchReshaped[1]][patchReshaped[0]], patchReshaped[:2]) else: - self._drawCharPatch(eventDc, isCursor, patch[2:], point) + self._drawPatch(eventDc, isCursor, patch[2:], canvas.map[patch[1]][patch[0]], point) return True else: return False diff --git a/libroar/RoarCanvasCommandsEdit.py b/libroar/RoarCanvasCommandsEdit.py index f634f33..3ce13d1 100644 --- a/libroar/RoarCanvasCommandsEdit.py +++ b/libroar/RoarCanvasCommandsEdit.py @@ -195,15 +195,13 @@ class RoarCanvasCommandsEdit(): @GuiCommandDecorator("Redo", "&Redo", ["", wx.ART_REDO], [wx.ACCEL_CTRL, ord("Y")], False) def canvasRedo(self, event): eventDc = self.parentCanvas.backend.getDeviceContext(self.parentCanvas.GetClientSize(), self.parentCanvas, self.parentCanvas.GetViewStart()) - self.parentCanvas.backend.drawCursorMaskWithJournal(self.parentCanvas.canvas, self.parentCanvas.canvas.journal, eventDc) - self.parentCanvas.dispatchDeltaPatches(self.parentCanvas.canvas.journal.popRedo()) + self.parentCanvas.dispatchDeltaPatches(self.parentCanvas.canvas.journal.popRedo(), eventDc=eventDc, forceDirtyCursor=True) self.update(size=self.parentCanvas.canvas.size, undoLevel=self.parentCanvas.canvas.journal.patchesUndoLevel) @GuiCommandDecorator("Undo", "&Undo", ["", wx.ART_UNDO], [wx.ACCEL_CTRL, ord("Z")], False) def canvasUndo(self, event): eventDc = self.parentCanvas.backend.getDeviceContext(self.parentCanvas.GetClientSize(), self.parentCanvas, self.parentCanvas.GetViewStart()) - self.parentCanvas.backend.drawCursorMaskWithJournal(self.parentCanvas.canvas, self.parentCanvas.canvas.journal, eventDc) - self.parentCanvas.dispatchDeltaPatches(self.parentCanvas.canvas.journal.popUndo()) + self.parentCanvas.dispatchDeltaPatches(self.parentCanvas.canvas.journal.popUndo(), eventDc=eventDc, forceDirtyCursor=True) self.update(size=self.parentCanvas.canvas.size, undoLevel=self.parentCanvas.canvas.journal.patchesUndoLevel) def __init__(self): diff --git a/libroar/RoarCanvasCommandsFile.py b/libroar/RoarCanvasCommandsFile.py index 2dbffd6..2dd9d6b 100644 --- a/libroar/RoarCanvasCommandsFile.py +++ b/libroar/RoarCanvasCommandsFile.py @@ -116,8 +116,9 @@ class RoarCanvasCommandsFile(): @GuiCommandDecorator("Exit", "E&xit", None, [wx.ACCEL_CTRL, ord("X")], None) def canvasExit(self, event): - if self._promptSaveChanges(): - self.parentFrame.Close(True) + if not self.exiting: + if self._promptSaveChanges(): + self.exiting = True; self.parentFrame.Close(True); @GuiCommandDecorator("Export as ANSI...", "Export as &ANSI...", None, None, None) def canvasExportAsAnsi(self, event): @@ -144,6 +145,8 @@ class RoarCanvasCommandsFile(): else: outPathName = dialog.GetPath(); self.lastDir = os.path.dirname(outPathName); self._storeLastDir(self.lastDir); self.parentCanvas.SetCursor(wx.Cursor(wx.CURSOR_WAIT)) + eventDc = self.parentCanvas.backend.getDeviceContext(self.parentCanvas.GetClientSize(), self.parentCanvas) + self.parentCanvas.backend.drawCursorMaskWithJournal(self.parentCanvas.canvas, self.parentCanvas.canvas.journal, eventDc) self.parentCanvas.canvas.exportStore.exportBitmapToPngFile(self.parentCanvas.backend.canvasBitmap, outPathName, wx.BITMAP_TYPE_PNG) self.parentCanvas.SetCursor(wx.Cursor(wx.NullCursor)) return True @@ -151,6 +154,8 @@ class RoarCanvasCommandsFile(): @GuiCommandDecorator("Export to Imgur...", "Export to I&mgur...", None, None, haveImgurApiKey and haveUrllib) def canvasExportImgur(self, event): self.parentCanvas.SetCursor(wx.Cursor(wx.CURSOR_WAIT)) + eventDc = self.parentCanvas.backend.getDeviceContext(self.parentCanvas.GetClientSize(), self.parentCanvas) + self.parentCanvas.backend.drawCursorMaskWithJournal(self.parentCanvas.canvas, self.parentCanvas.canvas.journal, eventDc) rc, status, result = self.parentCanvas.canvas.exportStore.exportBitmapToImgur(self.imgurApiKey, self.parentCanvas.backend.canvasBitmap, "", "", wx.BITMAP_TYPE_PNG) self.parentCanvas.SetCursor(wx.Cursor(wx.NullCursor)) if rc: @@ -291,5 +296,6 @@ class RoarCanvasCommandsFile(): ), ) self.toolBars = () + self.exiting = False # vim:expandtab foldmethod=marker sw=4 ts=4 tw=0 diff --git a/libroar/RoarCanvasWindow.py b/libroar/RoarCanvasWindow.py index 46a7166..4245549 100644 --- a/libroar/RoarCanvasWindow.py +++ b/libroar/RoarCanvasWindow.py @@ -111,12 +111,12 @@ class RoarCanvasWindow(GuiWindow): rc, dirty = tool.onKeyboardEvent(mapPoint, self.brushColours, self.brushPos, self.brushSize, self.canvas, self.dispatchPatchSingle, eventDc, keyChar, keyCode, keyModifiers, self.brushPos) elif mapPoint != None: self.dispatchPatchSingle(eventDc, True, [*mapPoint, self.brushColours[0], self.brushColours[0], 0, " "]) + self.canvas.journal.end() if dirty: self.dirty = True self.commands.update(dirty=self.dirty, cellPos=self.brushPos, undoLevel=self.canvas.journal.patchesUndoLevel) else: self.commands.update(cellPos=self.brushPos) - self.canvas.journal.end() if rc and (tool.__class__ == ToolObject): if tool.toolState > tool.TS_NONE: self.commands.update(undoInhibit=True) @@ -133,10 +133,11 @@ class RoarCanvasWindow(GuiWindow): eventDc.SetDeviceOrigin(*eventDcOrigin) return rc - def dispatchDeltaPatches(self, deltaPatches): - eventDc = self.backend.getDeviceContext(self.GetClientSize(), self) + def dispatchDeltaPatches(self, deltaPatches, eventDc=None, forceDirtyCursor=True): + if eventDc == None: + eventDc = self.backend.getDeviceContext(self.GetClientSize(), self) eventDcOrigin = eventDc.GetDeviceOrigin(); eventDc.SetDeviceOrigin(0, 0); - if self.canvas.dirtyCursor: + if self.canvas.dirtyCursor or forceDirtyCursor: self.backend.drawCursorMaskWithJournal(self.canvas, self.canvas.journal, eventDc) self.canvas.dirtyCursor = False for patch in deltaPatches: @@ -239,9 +240,7 @@ class RoarCanvasWindow(GuiWindow): def onLeaveWindow(self, event): if False: eventDc = self.backend.getDeviceContext(self.GetClientSize(), self, self.GetViewStart()) - eventDcOrigin = eventDc.GetDeviceOrigin(); eventDc.SetDeviceOrigin(0, 0); self.backend.drawCursorMaskWithJournal(self.canvas, self.canvas.journal, eventDc) - eventDc.SetDeviceOrigin(*eventDcOrigin) self.lastCellState = None def onMouseInput(self, event): @@ -278,9 +277,7 @@ class RoarCanvasWindow(GuiWindow): def onPaint(self, event): eventDc = self.backend.getDeviceContext(self.GetClientSize(), self) - eventDcOrigin = eventDc.GetDeviceOrigin(); eventDc.SetDeviceOrigin(0, 0); self.backend.drawCursorMaskWithJournal(self.canvas, self.canvas.journal, eventDc) - eventDc.SetDeviceOrigin(*eventDcOrigin) self.backend.onPaint(self.GetClientSize(), self, self.GetViewStart()) def __init__(self, backend, canvas, cellSize, commands, parent, parentFrame, pos, scrollStep, size): diff --git a/libroar/RoarClient.py b/libroar/RoarClient.py index 31afeef..c4d61a1 100644 --- a/libroar/RoarClient.py +++ b/libroar/RoarClient.py @@ -32,7 +32,11 @@ class RoarClient(GuiFrame): self.canvasPanel.GetEventHandler().ProcessEvent(event) def onClose(self, event): - if self.canvasPanel.commands._promptSaveChanges(): + if not self.canvasPanel.commands.exiting: + closeFlag = self.canvasPanel.commands._promptSaveChanges() + else: + closeFlag = True + if closeFlag: event.Skip(); def onSize(self, event):