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.
This commit is contained in:
Lucio Andrés Illanes Albornoz 2019-09-26 14:06:11 +02:00
parent 0750bc2261
commit 86e2c9e904
9 changed files with 86 additions and 105 deletions

View File

@ -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.)

View File

@ -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"

View File

@ -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":

View File

@ -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"],

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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):

View File

@ -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):