From 2ea89a0559539f44f4431a64060f217598ac9e5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Tue, 2 Jan 2018 16:19:07 +0100 Subject: [PATCH 001/148] Initial commit. --- mirc2png.py | 204 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 204 insertions(+) create mode 100755 mirc2png.py diff --git a/mirc2png.py b/mirc2png.py new file mode 100755 index 0000000..ab88412 --- /dev/null +++ b/mirc2png.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python3 +# +# mirc2png -- convert ASCII w/ mIRC control codes to monospaced PNG (for EFnet #MiRCART) +# Copyright (c) 2018 Lucio Andrés Illanes Albornoz +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +from enum import Enum +from PIL import Image, ImageDraw, ImageFont +import string, sys + +class MiRCART: + """Abstraction over ASCIIs containing mIRC control codes""" + + # {{{ mIRC colour number to RGBA map given ^B (bold) + ColourMapBold = [ + (255, 255, 255, 255), # White + (85, 85, 85, 255), # Grey + (85, 85, 255, 255), # Light Blue + (85, 255, 85, 255), # Light Green + (255, 85, 85, 255), # Light Red + (255, 85, 85, 255), # Light Red + (255, 85, 255, 255), # Pink + (255, 255, 85, 255), # Light Yellow + (255, 255, 85, 255), # Light Yellow + (85, 255, 85, 255), # Light Green + (85, 255, 255, 255), # Light Cyan + (85, 255, 255, 255), # Light Cyan + (85, 85, 255, 255), # Light Blue + (255, 85, 255, 255), # Pink + (85, 85, 85, 255), # Grey + (255, 255, 255, 255), # White + ] + # }}} + # {{{ mIRC colour number to RGBA map given none of ^[BFV_] (bold, italic, reverse, underline) + ColourMapNormal = [ + (255, 255, 255, 255), # White + (0, 0, 0, 255), # Black + (0, 0, 187, 255), # Blue + (0, 187, 0, 255), # Green + (255, 85, 85, 255), # Light Red + (187, 0, 0, 255), # Red + (187, 0, 187, 255), # Purple + (187, 187, 0, 255), # Yellow + (255, 255, 85, 255), # Light Yellow + (85, 255, 85, 255), # Light Green + (0, 187, 187, 255), # Cyan + (85, 255, 255, 255), # Light Cyan + (85, 85, 255, 255), # Light Blue + (255, 85, 255, 255), # Pink + (85, 85, 85, 255), # Grey + (187, 187, 187, 255), # Light Grey + ] + # }}} + # {{{ Parsing loop state + class State(Enum): + STATE_CHAR = 1 + STATE_COLOUR_SPEC = 2 + # }}} + + inFilePath = inFile = None; + inLines = inColsMax = inRows = None; + + outFontFilePath = outFontSize = None; + outImg = outImgDraw = outImgFont = None; + outCurColourBg = outCurColourFg = None; + outCurX = outCurY = None; + + inCurBold = inCurItalic = inCurReverse = inCurUnderline = None; + inCurColourSpec = None; + state = None; + inCurCol = None; + + # {{{ Calculate widest row in lines, ignoring non-printable & mIRC control code sequences + def getMaxCols(self, lines): + maxCols = 0; + for curRow in range(0, len(lines)): + curRowCols = 0; curState = self.State.STATE_CHAR; + curCol = 0; curColLen = len(lines[curRow]); + while curCol < curColLen: + curChar = lines[curRow][curCol] + if curState == self.State.STATE_CHAR: + if curChar == "": + curState = self.State.STATE_COLOUR_SPEC; curCol += 1; + elif curChar in string.printable: + curRowCols += 1; curCol += 1; + else: + curCol += 1; + elif curState == self.State.STATE_COLOUR_SPEC: + if curChar in set(",0123456789"): + curCol += 1; + else: + curState = self.State.STATE_CHAR; + maxCols = max(maxCols, curRowCols) + return maxCols + # }}} + # {{{ Parse single character as regular character and mutate state + def parseAsChar(self, char): + if char == "": + self.inCurCol += 1; self.inCurBold = 0 if self.inCurBold else 1; + elif char == "": + self.state = self.State.STATE_COLOUR_SPEC; self.inCurCol += 1; + elif char == "": + self.inCurCol += 1; self.inCurItalic = 0 if self.inCurItalic else 1; + elif char == "": + self.inCurCol += 1; + self.inCurBold = 0; self.inCurItalic = 0; self.inCurReverse = 0; self.inCurUnderline = 0; + self.inCurColourSpec = ""; + elif char == "": + self.inCurCol += 1; self.inCurReverse = 0 if self.inCurReverse else 1; + elif char == "": + self.inCurCol += 1; self.inCurUnderline = 0 if self.inCurUnderline else 1; + elif char == " ": + if self.inCurBold: + self.outImgDraw.rectangle(((self.outCurX, self.outCurY), (self.outCurX + 7, self.outCurY + 14)), fill=self.ColourMapBold[self.outCurColourBg]) + else: + self.outImgDraw.rectangle(((self.outCurX, self.outCurY), (self.outCurX + 7, self.outCurY + 14)), fill=self.ColourMapNormal[self.outCurColourBg]) + self.outCurX += 7; self.inCurCol += 1; + else: + if self.inCurBold: + self.outImgDraw.rectangle(((self.outCurX, self.outCurY), (self.outCurX + 7, self.outCurY + 14)), fill=self.ColourMapBold[self.outCurColourBg]) + # XXX implement italic, reverse, underline + self.outImgDraw.text((self.outCurX, self.outCurY), char, self.ColourMapBold[self.outCurColourFg], self.outImgFont) + else: + self.outImgDraw.rectangle(((self.outCurX, self.outCurY), (self.outCurX + 7, self.outCurY + 14)), fill=self.ColourMapNormal[self.outCurColourBg]) + # XXX implement italic, reverse, underline + self.outImgDraw.text((self.outCurX, self.outCurY), char, self.ColourMapNormal[self.outCurColourFg], self.outImgFont) + self.outCurX += 7; self.inCurCol += 1; + # }}} + # {{{ Parse single character as mIRC colour control code sequence and mutate state + def parseAsColourSpec(self, char): + if char in set(",0123456789"): + self.inCurColourSpec += char; self.inCurCol += 1; + else: + self.inCurColourSpec = self.inCurColourSpec.split(",") + if len(self.inCurColourSpec) == 2: + self.outCurColourFg = int(self.inCurColourSpec[0]) + self.outCurColourBg = int(self.inCurColourSpec[1] or self.outCurColourBg) + elif len(self.inCurColourSpec) == 1: + self.outCurColourFg = int(self.inCurColourSpec[0]) + else: + self.outCurColourBg = 1; self.outCurColourFg = 15; + self.inCurColourSpec = ""; self.state = self.State.STATE_CHAR; + # }}} + + # + # Initialisation method + def __init__(self, inFilePath, imgFilePath, fontFilePath, fontSize): + self.inFilePath = inFilePath; self.inFile = open(inFilePath, "r"); + self.inLines = self.inFile.readlines() + self.inColsMax = self.getMaxCols(self.inLines) + self.inRows = len(self.inLines) + self.outFontFilePath = fontFilePath; self.outFontSize = int(fontSize); + self.outImg = Image.new("RGBA", (self.inColsMax * 7, self.inRows * 14), self.ColourMapNormal[1]) + self.outImgDraw = ImageDraw.Draw(self.outImg) + self.outImgFont = ImageFont.truetype(self.outFontFilePath, self.outFontSize) + self.outCurColourBg = 1; self.outCurColourFg = 15; + self.outCurX = 0; self.outCurY = 0; + for inCurRow in range(0, len(self.inLines)): + self.inCurBold = 0; self.inCurItalic = 0; self.inCurReverse = 0; self.inCurUnderline = 0; + self.inCurColourSpec = ""; self.state = self.State.STATE_CHAR; + self.inCurCol = 0; + while self.inCurCol < len(self.inLines[inCurRow]): + if self.state == self.State.STATE_CHAR: + self.parseAsChar(self.inLines[inCurRow][self.inCurCol]) + elif self.state == self.State.STATE_COLOUR_SPEC: + self.parseAsColourSpec(self.inLines[inCurRow][self.inCurCol]) + self.outCurX = 0; self.outCurY += 14; + self.inFile.close(); + self.outImg.save(imgFilePath); + +# +# Entry point +def main(argv0, inFilePath, imgFilePath, fontFilePath, fontSize=11): + _MiRCART = MiRCART(inFilePath, imgFilePath, fontFilePath, fontSize) +if __name__ == "__main__": + if ((len(sys.argv) - 1) < 3)\ + or ((len(sys.argv) - 1) > 4): + print("usage: {} " \ + " " \ + " " \ + " " \ + "[]".format(sys.argv[0]), file=sys.stderr) + else: + main(*sys.argv) + +# vim:expandtab foldmethod=marker sw=8 ts=8 tw=120 From abaa92b53d09e7d30437c7e635ab7f32902c3021 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Tue, 2 Jan 2018 16:54:39 +0100 Subject: [PATCH 002/148] mirc2png.py:parseAsChar(): implement underline (^_.) mirc2png.py:__init__(): increment self.outCurY by correct row height. --- mirc2png.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/mirc2png.py b/mirc2png.py index ab88412..5442ad6 100755 --- a/mirc2png.py +++ b/mirc2png.py @@ -129,19 +129,25 @@ class MiRCART: self.inCurCol += 1; self.inCurUnderline = 0 if self.inCurUnderline else 1; elif char == " ": if self.inCurBold: - self.outImgDraw.rectangle(((self.outCurX, self.outCurY), (self.outCurX + 7, self.outCurY + 14)), fill=self.ColourMapBold[self.outCurColourBg]) + colourBg = self.ColourMapBold[self.outCurColourBg] else: - self.outImgDraw.rectangle(((self.outCurX, self.outCurY), (self.outCurX + 7, self.outCurY + 14)), fill=self.ColourMapNormal[self.outCurColourBg]) + colourBg = self.ColourMapNormal[self.outCurColourBg] + self.outImgDraw.rectangle(((self.outCurX, self.outCurY), (self.outCurX + 7, self.outCurY + 14)), fill=colourBg) + if self.inCurUnderline: + self.outImgDraw.line((self.outCurX, self.outCurY + 11, self.outCurX + 7, self.outCurY + 11), fill=colourFg) self.outCurX += 7; self.inCurCol += 1; else: if self.inCurBold: - self.outImgDraw.rectangle(((self.outCurX, self.outCurY), (self.outCurX + 7, self.outCurY + 14)), fill=self.ColourMapBold[self.outCurColourBg]) - # XXX implement italic, reverse, underline - self.outImgDraw.text((self.outCurX, self.outCurY), char, self.ColourMapBold[self.outCurColourFg], self.outImgFont) + colourBg = self.ColourMapBold[self.outCurColourBg] + colourFg = self.ColourMapBold[self.outCurColourFg] else: - self.outImgDraw.rectangle(((self.outCurX, self.outCurY), (self.outCurX + 7, self.outCurY + 14)), fill=self.ColourMapNormal[self.outCurColourBg]) - # XXX implement italic, reverse, underline - self.outImgDraw.text((self.outCurX, self.outCurY), char, self.ColourMapNormal[self.outCurColourFg], self.outImgFont) + colourBg = self.ColourMapNormal[self.outCurColourBg] + colourFg = self.ColourMapNormal[self.outCurColourFg] + self.outImgDraw.rectangle(((self.outCurX, self.outCurY), (self.outCurX + 7, self.outCurY + 14)), fill=colourBg) + # XXX implement italic, reverse + self.outImgDraw.text((self.outCurX, self.outCurY), char, colourFg, self.outImgFont) + if self.inCurUnderline: + self.outImgDraw.line((self.outCurX, self.outCurY + 11, self.outCurX + 7, self.outCurY + 11), fill=colourFg) self.outCurX += 7; self.inCurCol += 1; # }}} # {{{ Parse single character as mIRC colour control code sequence and mutate state @@ -182,7 +188,7 @@ class MiRCART: self.parseAsChar(self.inLines[inCurRow][self.inCurCol]) elif self.state == self.State.STATE_COLOUR_SPEC: self.parseAsColourSpec(self.inLines[inCurRow][self.inCurCol]) - self.outCurX = 0; self.outCurY += 14; + self.outCurX = 0; self.outCurY += 13; self.inFile.close(); self.outImg.save(imgFilePath); From 734cc1933341661c3412a2e35c5df93b7dd97a89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Tue, 2 Jan 2018 16:58:15 +0100 Subject: [PATCH 003/148] DejaVuSansMono.ttf: added to repository for convenience. mirc2png.py:main(): default fontFilePath to `DejaVuSansMono.ttf'. --- DejaVuSansMono.ttf | Bin 0 -> 340712 bytes mirc2png.py | 12 ++++++------ 2 files changed, 6 insertions(+), 6 deletions(-) create mode 100644 DejaVuSansMono.ttf diff --git a/DejaVuSansMono.ttf b/DejaVuSansMono.ttf new file mode 100644 index 0000000000000000000000000000000000000000..f5786022f18216b4c59c6fb0c634b52c8b6e7990 GIT binary patch literal 340712 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

JVuh<&iTWp{J;w*9G-ib5g= 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

( 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 z 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 literal 0 HcmV?d00001 diff --git a/mirc2png.py b/mirc2png.py index 5442ad6..4d88a5a 100755 --- a/mirc2png.py +++ b/mirc2png.py @@ -194,15 +194,15 @@ class MiRCART: # # Entry point -def main(argv0, inFilePath, imgFilePath, fontFilePath, fontSize=11): +def main(argv0, inFilePath, imgFilePath, fontFilePath="DejaVuSansMono.ttf", fontSize=11): _MiRCART = MiRCART(inFilePath, imgFilePath, fontFilePath, fontSize) if __name__ == "__main__": - if ((len(sys.argv) - 1) < 3)\ + if ((len(sys.argv) - 1) < 2)\ or ((len(sys.argv) - 1) > 4): - print("usage: {} " \ - " " \ - " " \ - " " \ + print("usage: {} " \ + " " \ + " " \ + "[] " \ "[]".format(sys.argv[0]), file=sys.stderr) else: main(*sys.argv) From 2d397ad4cfeeaa549c93c2d80ab5a8e74de6c83d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Tue, 2 Jan 2018 17:03:15 +0100 Subject: [PATCH 004/148] README.md: added to repository. --- README.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..1dc207d --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# mirc2png -- convert ASCII w/ mIRC control codes to monospaced PNG (for EFnet #MiRCART) +* Prerequisites: python3 && python3-pil on Debian-family Linux distributions +* Usage: ./mirc2png.py `` `` [``] [``] From 1c664f26e009c829cfd4b4c6b07131d87b8ed038 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Tue, 2 Jan 2018 19:16:09 +0100 Subject: [PATCH 005/148] .gitignore: added to repository. --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dbec55f --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.sw[op] From 89e244890c40df95f173c563f5d0ef2d31a6fa9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Tue, 2 Jan 2018 20:50:40 +0100 Subject: [PATCH 006/148] .gitignore: ignore __pycache__/. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index dbec55f..7a00c01 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ +__pycache__/ *.sw[op] From 93946c3bc9ef0808e1a1a3c5e4c9da3201142521 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Tue, 2 Jan 2018 20:51:06 +0100 Subject: [PATCH 007/148] mirc2png.py: fix class declaration & commenting style. --- mirc2png.py | 42 ++++++++++++++++++++---------------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/mirc2png.py b/mirc2png.py index 4d88a5a..7cd95f5 100755 --- a/mirc2png.py +++ b/mirc2png.py @@ -28,8 +28,20 @@ import string, sys class MiRCART: """Abstraction over ASCIIs containing mIRC control codes""" + inFilePath = inFile = None; + inLines = inColsMax = inRows = None; - # {{{ mIRC colour number to RGBA map given ^B (bold) + outFontFilePath = outFontSize = None; + outImg = outImgDraw = outImgFont = None; + outCurColourBg = outCurColourFg = None; + outCurX = outCurY = None; + + inCurBold = inCurItalic = inCurReverse = inCurUnderline = None; + inCurColourSpec = None; + state = None; + inCurCol = None; + + # {{{ ColourMapBold: mIRC colour number to RGBA map given ^B (bold) ColourMapBold = [ (255, 255, 255, 255), # White (85, 85, 85, 255), # Grey @@ -49,7 +61,7 @@ class MiRCART: (255, 255, 255, 255), # White ] # }}} - # {{{ mIRC colour number to RGBA map given none of ^[BFV_] (bold, italic, reverse, underline) + # {{{ ColourMapNormal: mIRC colour number to RGBA map given none of ^[BFV_] (bold, italic, reverse, underline) ColourMapNormal = [ (255, 255, 255, 255), # White (0, 0, 0, 255), # Black @@ -69,26 +81,13 @@ class MiRCART: (187, 187, 187, 255), # Light Grey ] # }}} - # {{{ Parsing loop state + # {{{ State: Parsing loop state class State(Enum): STATE_CHAR = 1 STATE_COLOUR_SPEC = 2 # }}} - inFilePath = inFile = None; - inLines = inColsMax = inRows = None; - - outFontFilePath = outFontSize = None; - outImg = outImgDraw = outImgFont = None; - outCurColourBg = outCurColourFg = None; - outCurX = outCurY = None; - - inCurBold = inCurItalic = inCurReverse = inCurUnderline = None; - inCurColourSpec = None; - state = None; - inCurCol = None; - - # {{{ Calculate widest row in lines, ignoring non-printable & mIRC control code sequences + # {{{ getMaxCols(): Calculate widest row in lines, ignoring non-printable & mIRC control code sequences def getMaxCols(self, lines): maxCols = 0; for curRow in range(0, len(lines)): @@ -111,7 +110,7 @@ class MiRCART: maxCols = max(maxCols, curRowCols) return maxCols # }}} - # {{{ Parse single character as regular character and mutate state + # {{{ parseAsChar(): Parse single character as regular character and mutate state def parseAsChar(self, char): if char == "": self.inCurCol += 1; self.inCurBold = 0 if self.inCurBold else 1; @@ -150,7 +149,7 @@ class MiRCART: self.outImgDraw.line((self.outCurX, self.outCurY + 11, self.outCurX + 7, self.outCurY + 11), fill=colourFg) self.outCurX += 7; self.inCurCol += 1; # }}} - # {{{ Parse single character as mIRC colour control code sequence and mutate state + # {{{ parseAsColourSpec(): Parse single character as mIRC colour control code sequence and mutate state def parseAsColourSpec(self, char): if char in set(",0123456789"): self.inCurColourSpec += char; self.inCurCol += 1; @@ -165,9 +164,7 @@ class MiRCART: self.outCurColourBg = 1; self.outCurColourFg = 15; self.inCurColourSpec = ""; self.state = self.State.STATE_CHAR; # }}} - - # - # Initialisation method + # {{{ Initialisation method def __init__(self, inFilePath, imgFilePath, fontFilePath, fontSize): self.inFilePath = inFilePath; self.inFile = open(inFilePath, "r"); self.inLines = self.inFile.readlines() @@ -191,6 +188,7 @@ class MiRCART: self.outCurX = 0; self.outCurY += 13; self.inFile.close(); self.outImg.save(imgFilePath); + # }}} # # Entry point From 5f4aa470b94c42d66b8892080366e2e75c7ddc90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Tue, 2 Jan 2018 20:51:32 +0100 Subject: [PATCH 008/148] README.md: add pngbot.py prerequisites. pngbot.py: initial commit (requires python3-{json,requests,urllib3}.) --- README.md | 2 +- pngbot.py | 135 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 1 deletion(-) create mode 100755 pngbot.py diff --git a/README.md b/README.md index 1dc207d..2272eba 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # mirc2png -- convert ASCII w/ mIRC control codes to monospaced PNG (for EFnet #MiRCART) -* Prerequisites: python3 && python3-pil on Debian-family Linux distributions +* Prerequisites: python3 && python3-pil (&& python3-{json,requests,urllib3} for pngbot.py) on Debian-family Linux distributions * Usage: ./mirc2png.py `` `` [``] [``] diff --git a/pngbot.py b/pngbot.py new file mode 100755 index 0000000..8a6a45e --- /dev/null +++ b/pngbot.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +# +# mirc2png -- convert ASCII w/ mIRC control codes to monospaced PNG (for EFnet #MiRCART) +# Copyright (c) 2018 Lucio Andrés Illanes Albornoz +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +from itertools import chain +import base64 +import json +import mirc2png +import os, socket, sys +import requests +import urllib.request + +class IrcBot: + """Blocking abstraction over the IRC protocol""" + serverHname = serverPort = None; + clientNick = clientIdent = clientGecos = None; + clientSocket = clientSocketFile = None; + + # {{{ connect(): Connect to server and register + def connect(self): + self.clientSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.clientSocket.connect((self.serverHname, int(self.serverPort))) + self.clientSocketFile = self.clientSocket.makefile() + self.sendline("NICK", self.clientNick) + self.sendline("USER", self.clientIdent, "0", "0", self.clientGecos) + # }}} + # {{{ close(): Close connection to server + def close(self): + self.clientSocket.close() + self.clientSocket = self.clientSocketFile = None; + # }}} + # {{{ readline(): Read and parse single line from server into canonicalised list + def readline(self): + msg = self.clientSocketFile.readline() + if len(msg): + msg = msg.rstrip("\r\n") + else: + return None + msg = msg.split(" :", 1) + if len(msg) == 1: + msg = list(chain.from_iterable(m.split(" ") for m in msg)) + elif len(msg) == 2: + msg = msg[0].split(" ") + [msg[1]] + if msg[0][0] == ':': + msg = [msg[0][1:]] + msg[1:] + else: + msg = [""] + msg[1:] + return msg + # }}} + # {{{ sendline(): Parse and send single line to server from list + def sendline(self, *args): + msg = ""; argNumMax = len(args); + for argNum in range(0, argNumMax): + if argNum == (argNumMax - 1): + msg += ":" + args[argNum] + else: + msg += args[argNum] + " " + return self.clientSocket.send((msg + "\r\n").encode()) + # }}} + # {{{ Initialisation method + def __init__(self, serverHname, serverPort, clientNick, clientIdent, clientGecos): + self.serverHname = serverHname; self.serverPort = serverPort; + self.clientNick = clientNick; self.clientIdent = clientIdent; self.clientGecos = clientGecos; + # }}} + +# +# Entry point +def main(argv0, ircServerHname, ircServerPort="6667", ircClientNick="pngbot", ircClientIdent="pngbot", ircClientGecos="pngbot", ircClientChannel="#MiRCART"): + _IrcBot = IrcBot(ircServerHname, ircServerPort, ircClientNick, ircClientIdent, ircClientGecos) + print("Connecting to {}:{}...".format(ircServerHname, ircServerPort)) + _IrcBot.connect() + print("Connected to {}:{}.".format(ircServerHname, ircServerPort)) + print("Registering on {}:{} as {}, {}, {}...".format(ircServerHname, ircServerPort, ircClientNick, ircClientIdent, ircClientGecos)) + while True: + ircServerMessage = _IrcBot.readline() + if ircServerMessage == None: + print("Disconnected from {}:{}.".format(ircServerHname, ircServerPort)) + _IrcBot.close(); break; + elif ircServerMessage[1] == "001": + print("Registered on {}:{} as {}, {}, {}.".format(ircServerHname, ircServerPort, ircClientNick, ircClientIdent, ircClientGecos)) + print("Joining {} on {}:{}...".format(ircClientChannel, ircServerHname, ircServerPort)) + _IrcBot.sendline("JOIN", ircClientChannel) + elif ircServerMessage[1] == "PING": + _IrcBot.sendline("PONG", ircServerMessage[2]) + elif ircServerMessage[1] == "PRIVMSG" \ + and ircServerMessage[2] == ircClientChannel \ + and ircServerMessage[3].startswith("!pngbot "): + asciiUrl = ircServerMessage[3].split(" ")[1] + asciiTmpFilePath = "tmp.txt"; imgTmpFilePath = "tmp.png"; + if os.path.isfile(asciiTmpFilePath): + os.remove(asciiTmpFilePath) + if os.path.isfile(imgTmpFilePath): + os.remove(imgTmpFilePath) + urllib.request.urlretrieve(asciiUrl, asciiTmpFilePath) + _MiRCART = mirc2png.MiRCART(asciiTmpFilePath, imgTmpFilePath, "DejaVuSansMono.ttf", 11) + imgurResponseHttp = requests.post("https://api.imgur.com/3/upload.json", data={"key":"c9a6efb3d7932fd", "image":base64.b64encode(open(imgTmpFilePath, "rb").read()), "type":"base64", "name":"tmp.png", "title":"tmp.png"}, headers={"Authorization": "Client-ID c9a6efb3d7932fd"}) + imgurResponse = json.loads(imgurResponseHttp.text) + imgurResponseUrl = imgurResponse.get("data").get("link") + _IrcBot.sendline("PRIVMSG", ircServerMessage[2], "Uploaded as {}".format(imgurResponseUrl)) + os.remove(asciiTmpFilePath); os.remove(imgTmpFilePath); + +if __name__ == "__main__": + if ((len(sys.argv) - 1) < 1)\ + or ((len(sys.argv) - 1) > 4): + print("usage: {} " \ + " " \ + "[] " \ + "[] " \ + "[] " \ + "[] " \ + "[] ".format(sys.argv[0]), file=sys.stderr) + else: + main(*sys.argv) + +# vim:expandtab foldmethod=marker sw=8 ts=8 tw=120 From b94d3afdb5c4c659061dcb72f1556d10836357f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Tue, 2 Jan 2018 21:54:02 +0100 Subject: [PATCH 009/148] pngbot.py: convert to lower case when comparing channel names. --- pngbot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pngbot.py b/pngbot.py index 8a6a45e..afddc1a 100755 --- a/pngbot.py +++ b/pngbot.py @@ -102,8 +102,8 @@ def main(argv0, ircServerHname, ircServerPort="6667", ircClientNick="pngbot", ir _IrcBot.sendline("JOIN", ircClientChannel) elif ircServerMessage[1] == "PING": _IrcBot.sendline("PONG", ircServerMessage[2]) - elif ircServerMessage[1] == "PRIVMSG" \ - and ircServerMessage[2] == ircClientChannel \ + elif ircServerMessage[1] == "PRIVMSG" \ + and ircServerMessage[2].lower() == ircClientChannel.lower() \ and ircServerMessage[3].startswith("!pngbot "): asciiUrl = ircServerMessage[3].split(" ")[1] asciiTmpFilePath = "tmp.txt"; imgTmpFilePath = "tmp.png"; From e091920bda5ec3f40af3f60e0a8da2f5ff9acb1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Tue, 2 Jan 2018 21:57:21 +0100 Subject: [PATCH 010/148] pngbot.py: handle HTTP status codes differing from 200. pngbot.py: update IRC message text. --- pngbot.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pngbot.py b/pngbot.py index afddc1a..5bc33b7 100755 --- a/pngbot.py +++ b/pngbot.py @@ -115,8 +115,11 @@ def main(argv0, ircServerHname, ircServerPort="6667", ircClientNick="pngbot", ir _MiRCART = mirc2png.MiRCART(asciiTmpFilePath, imgTmpFilePath, "DejaVuSansMono.ttf", 11) imgurResponseHttp = requests.post("https://api.imgur.com/3/upload.json", data={"key":"c9a6efb3d7932fd", "image":base64.b64encode(open(imgTmpFilePath, "rb").read()), "type":"base64", "name":"tmp.png", "title":"tmp.png"}, headers={"Authorization": "Client-ID c9a6efb3d7932fd"}) imgurResponse = json.loads(imgurResponseHttp.text) - imgurResponseUrl = imgurResponse.get("data").get("link") - _IrcBot.sendline("PRIVMSG", ircServerMessage[2], "Uploaded as {}".format(imgurResponseUrl)) + if imgurResponse.status_code == 200: + imgurResponseUrl = imgurResponse.get("data").get("link") + _IrcBot.sendline("PRIVMSG", ircServerMessage[2], "8/!\\ Uploaded as: {}".format(imgurResponseUrl)) + else: + _IrcBot.sendline("PRIVMSG", ircServerMessage[2], "4/!\\ Uploaded failed with HTTP status code {}!".format(imgurResponse.status_code)) os.remove(asciiTmpFilePath); os.remove(imgTmpFilePath); if __name__ == "__main__": From 4c5755d2fc552f46903ec783bfefd1c7d80c9cb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Tue, 2 Jan 2018 22:02:36 +0100 Subject: [PATCH 011/148] pngbot.py: rate-limit `!pngbot' processing to once (1) each 45 seconds. --- pngbot.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pngbot.py b/pngbot.py index 5bc33b7..16cfa19 100755 --- a/pngbot.py +++ b/pngbot.py @@ -26,7 +26,7 @@ from itertools import chain import base64 import json import mirc2png -import os, socket, sys +import os, socket, sys, time import requests import urllib.request @@ -92,6 +92,7 @@ def main(argv0, ircServerHname, ircServerPort="6667", ircClientNick="pngbot", ir print("Connected to {}:{}.".format(ircServerHname, ircServerPort)) print("Registering on {}:{} as {}, {}, {}...".format(ircServerHname, ircServerPort, ircClientNick, ircClientIdent, ircClientGecos)) while True: + ircClientLastMessage = int(time.time()) ircServerMessage = _IrcBot.readline() if ircServerMessage == None: print("Disconnected from {}:{}.".format(ircServerHname, ircServerPort)) @@ -105,6 +106,10 @@ def main(argv0, ircServerHname, ircServerPort="6667", ircClientNick="pngbot", ir elif ircServerMessage[1] == "PRIVMSG" \ and ircServerMessage[2].lower() == ircClientChannel.lower() \ and ircServerMessage[3].startswith("!pngbot "): + if (int(time.time()) - ircClientLastMessage) < 45: + continue + else: + ircClientLastMessage = int(time.time()) asciiUrl = ircServerMessage[3].split(" ")[1] asciiTmpFilePath = "tmp.txt"; imgTmpFilePath = "tmp.png"; if os.path.isfile(asciiTmpFilePath): From 4f56b9c78393d1ace6c7309d69c9a2b3ab5d7880 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Tue, 2 Jan 2018 22:20:20 +0100 Subject: [PATCH 012/148] pngbot.py: splits main into class IrcMiRCARTBot. pngbot.py:IrcBot.close(): only close() clientSocket if non-None. pngbot.py:IrcMiRCARTBot.connect(): correctly zero-initialise clientLastMessage. pngbot.py:IrcMiRCARTBot.dispatch(): fix imgurResponseHttp.status_code reference. --- pngbot.py | 103 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 62 insertions(+), 41 deletions(-) mode change 100755 => 100644 pngbot.py diff --git a/pngbot.py b/pngbot.py old mode 100755 new mode 100644 index 16cfa19..aed396f --- a/pngbot.py +++ b/pngbot.py @@ -46,7 +46,8 @@ class IrcBot: # }}} # {{{ close(): Close connection to server def close(self): - self.clientSocket.close() + if self.clientSocket != None: + self.clientSocket.close() self.clientSocket = self.clientSocketFile = None; # }}} # {{{ readline(): Read and parse single line from server into canonicalised list @@ -83,49 +84,69 @@ class IrcBot: self.clientNick = clientNick; self.clientIdent = clientIdent; self.clientGecos = clientGecos; # }}} +class IrcMiRCARTBot(IrcBot): + """IRC<->MiRCART bot""" + clientLastMessage = clientChannel = None + + # {{{ connect(): Connect to server and (re)initialise + def connect(self): + print("Connecting to {}:{}...".format(self.serverHname, self.serverPort)) + super().connect() + print("Connected to {}:{}.".format(self.serverHname, self.serverPort)) + print("Registering on {}:{} as {}, {}, {}...".format(self.serverHname, self.serverPort, self.clientNick, self.clientIdent, self.clientGecos)) + self.clientLastMessage = 0 + # }}} + # {{{ dispatch(): Read, parse, and dispatch single line from server + def dispatch(self): + while True: + serverMessage = self.readline() + if serverMessage == None: + print("Disconnected from {}:{}.".format(self.serverHname, self.serverPort)) + self.close(); break; + elif serverMessage[1] == "001": + print("Registered on {}:{} as {}, {}, {}.".format(self.serverHname, self.serverPort, self.clientNick, self.clientIdent, self.clientGecos)) + print("Joining {} on {}:{}...".format(self.clientChannel, self.serverHname, self.serverPort)) + self.sendline("JOIN", self.clientChannel) + elif serverMessage[1] == "PING": + self.sendline("PONG", serverMessage[2]) + elif serverMessage[1] == "PRIVMSG" \ + and serverMessage[2].lower() == self.clientChannel.lower() \ + and serverMessage[3].startswith("!pngbot "): + if (int(time.time()) - self.clientLastMessage) < 45: + continue + else: + self.clientLastMessage = int(time.time()) + asciiUrl = serverMessage[3].split(" ")[1] + asciiTmpFilePath = "tmp.txt"; imgTmpFilePath = "tmp.png"; + if os.path.isfile(asciiTmpFilePath): + os.remove(asciiTmpFilePath) + if os.path.isfile(imgTmpFilePath): + os.remove(imgTmpFilePath) + urllib.request.urlretrieve(asciiUrl, asciiTmpFilePath) + _MiRCART = mirc2png.MiRCART(asciiTmpFilePath, imgTmpFilePath, "DejaVuSansMono.ttf", 11) + imgurResponseHttp = requests.post("https://api.imgur.com/3/upload.json", data={"key":"c9a6efb3d7932fd", "image":base64.b64encode(open(imgTmpFilePath, "rb").read()), "type":"base64", "name":"tmp.png", "title":"tmp.png"}, headers={"Authorization": "Client-ID c9a6efb3d7932fd"}) + imgurResponse = json.loads(imgurResponseHttp.text) + if imgurResponseHttp.status_code == 200: + imgurResponseUrl = imgurResponse.get("data").get("link") + self.sendline("PRIVMSG", serverMessage[2], "8/!\\ Uploaded as: {}".format(imgurResponseUrl)) + else: + self.sendline("PRIVMSG", serverMessage[2], "4/!\\ Uploaded failed with HTTP status code {}!".format(imgurResponseHttp.status_code)) + os.remove(asciiTmpFilePath); os.remove(imgTmpFilePath); + # }}} + # {{{ Initialisation method + def __init__(self, serverHname, serverPort="6667", clientNick="pngbot", clientIdent="pngbot", clientGecos="pngbot", clientChannel="#MiRCART"): + super().__init__(serverHname, serverPort, clientNick, clientIdent, clientGecos) + self.clientChannel = clientChannel + # }}} + # # Entry point -def main(argv0, ircServerHname, ircServerPort="6667", ircClientNick="pngbot", ircClientIdent="pngbot", ircClientGecos="pngbot", ircClientChannel="#MiRCART"): - _IrcBot = IrcBot(ircServerHname, ircServerPort, ircClientNick, ircClientIdent, ircClientGecos) - print("Connecting to {}:{}...".format(ircServerHname, ircServerPort)) - _IrcBot.connect() - print("Connected to {}:{}.".format(ircServerHname, ircServerPort)) - print("Registering on {}:{} as {}, {}, {}...".format(ircServerHname, ircServerPort, ircClientNick, ircClientIdent, ircClientGecos)) +def main(*argv): + _IrcMiRCARTBot = IrcMiRCARTBot(*argv[1:]) while True: - ircClientLastMessage = int(time.time()) - ircServerMessage = _IrcBot.readline() - if ircServerMessage == None: - print("Disconnected from {}:{}.".format(ircServerHname, ircServerPort)) - _IrcBot.close(); break; - elif ircServerMessage[1] == "001": - print("Registered on {}:{} as {}, {}, {}.".format(ircServerHname, ircServerPort, ircClientNick, ircClientIdent, ircClientGecos)) - print("Joining {} on {}:{}...".format(ircClientChannel, ircServerHname, ircServerPort)) - _IrcBot.sendline("JOIN", ircClientChannel) - elif ircServerMessage[1] == "PING": - _IrcBot.sendline("PONG", ircServerMessage[2]) - elif ircServerMessage[1] == "PRIVMSG" \ - and ircServerMessage[2].lower() == ircClientChannel.lower() \ - and ircServerMessage[3].startswith("!pngbot "): - if (int(time.time()) - ircClientLastMessage) < 45: - continue - else: - ircClientLastMessage = int(time.time()) - asciiUrl = ircServerMessage[3].split(" ")[1] - asciiTmpFilePath = "tmp.txt"; imgTmpFilePath = "tmp.png"; - if os.path.isfile(asciiTmpFilePath): - os.remove(asciiTmpFilePath) - if os.path.isfile(imgTmpFilePath): - os.remove(imgTmpFilePath) - urllib.request.urlretrieve(asciiUrl, asciiTmpFilePath) - _MiRCART = mirc2png.MiRCART(asciiTmpFilePath, imgTmpFilePath, "DejaVuSansMono.ttf", 11) - imgurResponseHttp = requests.post("https://api.imgur.com/3/upload.json", data={"key":"c9a6efb3d7932fd", "image":base64.b64encode(open(imgTmpFilePath, "rb").read()), "type":"base64", "name":"tmp.png", "title":"tmp.png"}, headers={"Authorization": "Client-ID c9a6efb3d7932fd"}) - imgurResponse = json.loads(imgurResponseHttp.text) - if imgurResponse.status_code == 200: - imgurResponseUrl = imgurResponse.get("data").get("link") - _IrcBot.sendline("PRIVMSG", ircServerMessage[2], "8/!\\ Uploaded as: {}".format(imgurResponseUrl)) - else: - _IrcBot.sendline("PRIVMSG", ircServerMessage[2], "4/!\\ Uploaded failed with HTTP status code {}!".format(imgurResponse.status_code)) - os.remove(asciiTmpFilePath); os.remove(imgTmpFilePath); + _IrcMiRCARTBot.connect() + _IrcMiRCARTBot.dispatch() + _IrcMiRCARTBot.close() if __name__ == "__main__": if ((len(sys.argv) - 1) < 1)\ From 3b29343e151346d767909f9377293e7500490228 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Tue, 2 Jan 2018 22:24:21 +0100 Subject: [PATCH 013/148] mirc2png.py:MiRCART.__init__(): move defaults from main(). --- mirc2png.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mirc2png.py b/mirc2png.py index 7cd95f5..d88ce8b 100755 --- a/mirc2png.py +++ b/mirc2png.py @@ -165,7 +165,7 @@ class MiRCART: self.inCurColourSpec = ""; self.state = self.State.STATE_CHAR; # }}} # {{{ Initialisation method - def __init__(self, inFilePath, imgFilePath, fontFilePath, fontSize): + def __init__(self, inFilePath, imgFilePath, fontFilePath="DejaVuSansMono.ttf", fontSize=11): self.inFilePath = inFilePath; self.inFile = open(inFilePath, "r"); self.inLines = self.inFile.readlines() self.inColsMax = self.getMaxCols(self.inLines) @@ -192,8 +192,8 @@ class MiRCART: # # Entry point -def main(argv0, inFilePath, imgFilePath, fontFilePath="DejaVuSansMono.ttf", fontSize=11): - _MiRCART = MiRCART(inFilePath, imgFilePath, fontFilePath, fontSize) +def main(*argv): + _MiRCART = MiRCART(*argv[1:]) if __name__ == "__main__": if ((len(sys.argv) - 1) < 2)\ or ((len(sys.argv) - 1) > 4): From 4556f8b238278a8c44a2bae8cb0adcec0d3bcc91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Tue, 2 Jan 2018 22:25:00 +0100 Subject: [PATCH 014/148] pngbot.py: set +x bit. --- pngbot.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 pngbot.py diff --git a/pngbot.py b/pngbot.py old mode 100644 new mode 100755 From 75e8e5fdf0241d603d2a35f86a3e3ff66fb7ecd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Tue, 2 Jan 2018 22:38:10 +0100 Subject: [PATCH 015/148] pngbot.py:IrcMiRCARTBot.uploadToImgur(): split from dispatch(). pngbot.py:IrcMiRCARTBot.dispatch(): only os.remove() if target file exists. --- pngbot.py | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/pngbot.py b/pngbot.py index aed396f..0f95c99 100755 --- a/pngbot.py +++ b/pngbot.py @@ -124,14 +124,33 @@ class IrcMiRCARTBot(IrcBot): os.remove(imgTmpFilePath) urllib.request.urlretrieve(asciiUrl, asciiTmpFilePath) _MiRCART = mirc2png.MiRCART(asciiTmpFilePath, imgTmpFilePath, "DejaVuSansMono.ttf", 11) - imgurResponseHttp = requests.post("https://api.imgur.com/3/upload.json", data={"key":"c9a6efb3d7932fd", "image":base64.b64encode(open(imgTmpFilePath, "rb").read()), "type":"base64", "name":"tmp.png", "title":"tmp.png"}, headers={"Authorization": "Client-ID c9a6efb3d7932fd"}) - imgurResponse = json.loads(imgurResponseHttp.text) - if imgurResponseHttp.status_code == 200: - imgurResponseUrl = imgurResponse.get("data").get("link") - self.sendline("PRIVMSG", serverMessage[2], "8/!\\ Uploaded as: {}".format(imgurResponseUrl)) + imgurResponse = self.uploadToImgur(imgTmpFilePath, "MiRCART image", "MiRCART image", "c9a6efb3d7932fd") + if imgurResponse[0] == 200: + self.sendline("PRIVMSG", serverMessage[2], "8/!\\ Uploaded as: {}".format(imgurResponse[1])) else: - self.sendline("PRIVMSG", serverMessage[2], "4/!\\ Uploaded failed with HTTP status code {}!".format(imgurResponseHttp.status_code)) - os.remove(asciiTmpFilePath); os.remove(imgTmpFilePath); + self.sendline("PRIVMSG", serverMessage[2], "4/!\\ Uploaded failed with HTTP status code {}!".format(imgurResponse[0])) + if os.path.isfile(asciiTmpFilePath): + os.remove(asciiTmpFilePath) + if os.path.isfile(imgTmpFilePath): + os.remove(imgTmpFilePath) + # }}} + # {{{ uploadToImgur(): Upload single file to Imgur + def uploadToImgur(self, imgFilePath, imgName, imgTitle, apiKey): + requestImageData = open(imgFilePath, "rb").read() + requestData = { \ + "image": base64.b64encode(requestImageData), \ + "key": apiKey, \ + "name": imgName, \ + "title": imgTitle, \ + "type": "base64"} + requestHeaders = { \ + "Authorization": "Client-ID " + apiKey} + responseHttp = requests.post("https://api.imgur.com/3/upload.json", data=requestData, headers=requestHeaders) + responseDict = json.loads(responseHttp.text) + if responseHttp.status_code == 200: + return [200, responseDict.get("data").get("link")] + else: + return [responseHttp.status_code] # }}} # {{{ Initialisation method def __init__(self, serverHname, serverPort="6667", clientNick="pngbot", clientIdent="pngbot", clientGecos="pngbot", clientChannel="#MiRCART"): From 993e986ec03fa523127f536e1ba62d8a6f9ec579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Tue, 2 Jan 2018 23:12:26 +0100 Subject: [PATCH 016/148] pngbot.py:IrcMiRCARTBot.clientChannelOps: added to track nicks authorised to use `!pngbot'. pngbot.py:IrcMiRCARTBot.dispatch(): sync clientChannelOps w/ 353 (RPL_NAMREPLY) and (channel) MODE messages. --- pngbot.py | 51 +++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 41 insertions(+), 10 deletions(-) diff --git a/pngbot.py b/pngbot.py index 0f95c99..4c51e26 100755 --- a/pngbot.py +++ b/pngbot.py @@ -86,7 +86,7 @@ class IrcBot: class IrcMiRCARTBot(IrcBot): """IRC<->MiRCART bot""" - clientLastMessage = clientChannel = None + clientChannelLastMessage = clientChannelOps = clientChannel = None # {{{ connect(): Connect to server and (re)initialise def connect(self): @@ -94,7 +94,7 @@ class IrcMiRCARTBot(IrcBot): super().connect() print("Connected to {}:{}.".format(self.serverHname, self.serverPort)) print("Registering on {}:{} as {}, {}, {}...".format(self.serverHname, self.serverPort, self.clientNick, self.clientIdent, self.clientGecos)) - self.clientLastMessage = 0 + self.clientLastMessage = 0; self.clientChannelOps = []; # }}} # {{{ dispatch(): Read, parse, and dispatch single line from server def dispatch(self): @@ -107,10 +107,41 @@ class IrcMiRCARTBot(IrcBot): print("Registered on {}:{} as {}, {}, {}.".format(self.serverHname, self.serverPort, self.clientNick, self.clientIdent, self.clientGecos)) print("Joining {} on {}:{}...".format(self.clientChannel, self.serverHname, self.serverPort)) self.sendline("JOIN", self.clientChannel) + elif serverMessage[1] == "353" \ + and serverMessage[4].lower() == self.clientChannel.lower(): + for channelNickSpec in serverMessage[5].split(" "): + if channelNickSpec[0] == "@" \ + and len(channelNickSpec[1:]): + self.clientChannelOps.append(channelNickSpec[1:]) + print("Authorising {} on {}".format(channelNickSpec[1:], serverMessage[2])) + elif serverMessage[1] == "MODE" \ + and serverMessage[2].lower() == self.clientChannel.lower(): + channelModeType = "+"; channelModeArg = 4; + channelAuthAdd = ""; channelAuthDel = ""; + for channelModeChar in serverMessage[3]: + if channelModeChar[0] == "-": + channelModeType = "-" + elif channelModeChar[0] == "+": + channelModeType = "+" + elif channelModeChar[0].isalpha(): + if channelModeChar[0] == "o": + if channelModeType == "+": + channelAuthAdd = serverMessage[channelModeArg]; channelAuthDel = ""; + elif channelModeType == "-": + channelAuthAdd = ""; channelAuthDel = serverMessage[channelModeArg]; + channelModeArg += 1 + if len(channelAuthAdd) \ + and channelAuthAdd not in self.clientChannelOps: + print("Authorising {} on {}".format(channelAuthAdd, serverMessage[2])) + self.clientChannelOps.append(channelAuthAdd) + elif len(channelAuthDel) \ + and channelAuthDel in self.clientChannelOps: + print("Deauthorising {} on {}".format(channelAuthDel, serverMessage[2])) + self.clientChannelOps.remove(channelAuthDel) elif serverMessage[1] == "PING": self.sendline("PONG", serverMessage[2]) - elif serverMessage[1] == "PRIVMSG" \ - and serverMessage[2].lower() == self.clientChannel.lower() \ + elif serverMessage[1] == "PRIVMSG" \ + and serverMessage[2].lower() == self.clientChannel.lower() \ and serverMessage[3].startswith("!pngbot "): if (int(time.time()) - self.clientLastMessage) < 45: continue @@ -170,12 +201,12 @@ def main(*argv): if __name__ == "__main__": if ((len(sys.argv) - 1) < 1)\ or ((len(sys.argv) - 1) > 4): - print("usage: {} " \ - " " \ - "[] " \ - "[] " \ - "[] " \ - "[] " \ + print("usage: {} " \ + " " \ + "[] " \ + "[] " \ + "[] " \ + "[] " \ "[] ".format(sys.argv[0]), file=sys.stderr) else: main(*sys.argv) From 28e5b9b9585e8fa8572118790895490949d73583 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Tue, 2 Jan 2018 23:23:30 +0100 Subject: [PATCH 017/148] pngbot.py:IrcMiRCARTBot.dispatch(): ignore `!pngbot' requests from unauthorised (non-opped) nicks. pngbot.py:IrcMiRCARTBot.dispatch(): add status (stdout) messages. pngbot.py:IrcMiRCARTBot.dispatch(): always use lower-case channel name in status (stdout) messages. --- pngbot.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/pngbot.py b/pngbot.py index 4c51e26..e32f521 100755 --- a/pngbot.py +++ b/pngbot.py @@ -112,8 +112,8 @@ class IrcMiRCARTBot(IrcBot): for channelNickSpec in serverMessage[5].split(" "): if channelNickSpec[0] == "@" \ and len(channelNickSpec[1:]): - self.clientChannelOps.append(channelNickSpec[1:]) - print("Authorising {} on {}".format(channelNickSpec[1:], serverMessage[2])) + self.clientChannelOps.append(channelNickSpec[1:].lower()) + print("Authorising {} on {}".format(channelNickSpec[1:].lower(), serverMessage[2].lower())) elif serverMessage[1] == "MODE" \ and serverMessage[2].lower() == self.clientChannel.lower(): channelModeType = "+"; channelModeArg = 4; @@ -132,11 +132,13 @@ class IrcMiRCARTBot(IrcBot): channelModeArg += 1 if len(channelAuthAdd) \ and channelAuthAdd not in self.clientChannelOps: - print("Authorising {} on {}".format(channelAuthAdd, serverMessage[2])) + channelAuthAdd = channelAuthAdd.lower() + print("Authorising {} on {}".format(channelAuthAdd, serverMessage[2].lower())) self.clientChannelOps.append(channelAuthAdd) elif len(channelAuthDel) \ and channelAuthDel in self.clientChannelOps: - print("Deauthorising {} on {}".format(channelAuthDel, serverMessage[2])) + channelAuthDel = channelAuthDel.lower() + print("Deauthorising {} on {}".format(channelAuthDel, serverMessage[2].lower())) self.clientChannelOps.remove(channelAuthDel) elif serverMessage[1] == "PING": self.sendline("PONG", serverMessage[2]) @@ -144,8 +146,13 @@ class IrcMiRCARTBot(IrcBot): and serverMessage[2].lower() == self.clientChannel.lower() \ and serverMessage[3].startswith("!pngbot "): if (int(time.time()) - self.clientLastMessage) < 45: + print("Ignoring request on {} from {} due to rate limit: {}".format(serverMessage[2].lower(), serverMessage[0], serverMessage[3])) + continue + elif serverMessage[0].split("!")[0].lower() not in self.clientChannelOps: + print("Ignoring request on {} from {} due to lack of authorisation: {}".format(serverMessage[2].lower(), serverMessage[0], serverMessage[3])) continue else: + print("Processing request on {} from {}: {}".format(serverMessage[2].lower(), serverMessage[0], serverMessage[3])) self.clientLastMessage = int(time.time()) asciiUrl = serverMessage[3].split(" ")[1] asciiTmpFilePath = "tmp.txt"; imgTmpFilePath = "tmp.png"; @@ -157,8 +164,10 @@ class IrcMiRCARTBot(IrcBot): _MiRCART = mirc2png.MiRCART(asciiTmpFilePath, imgTmpFilePath, "DejaVuSansMono.ttf", 11) imgurResponse = self.uploadToImgur(imgTmpFilePath, "MiRCART image", "MiRCART image", "c9a6efb3d7932fd") if imgurResponse[0] == 200: + print("Uploaded as: {}".format(imgurResponse[1])) self.sendline("PRIVMSG", serverMessage[2], "8/!\\ Uploaded as: {}".format(imgurResponse[1])) else: + print("Upload failed with HTTP status code {}".format(imgurResponse[0])) self.sendline("PRIVMSG", serverMessage[2], "4/!\\ Uploaded failed with HTTP status code {}!".format(imgurResponse[0])) if os.path.isfile(asciiTmpFilePath): os.remove(asciiTmpFilePath) From f4218e8ae1d0b382604b37558a71661e49a7ba5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Tue, 2 Jan 2018 23:35:01 +0100 Subject: [PATCH 018/148] pngbot.py:IrcMiRCARTBot.dispatch{001,353,MODE,None,PING,PRIVMSG}(): split from dispatch(). --- pngbot.py | 164 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 94 insertions(+), 70 deletions(-) diff --git a/pngbot.py b/pngbot.py index e32f521..663ac58 100755 --- a/pngbot.py +++ b/pngbot.py @@ -96,83 +96,107 @@ class IrcMiRCARTBot(IrcBot): print("Registering on {}:{} as {}, {}, {}...".format(self.serverHname, self.serverPort, self.clientNick, self.clientIdent, self.clientGecos)) self.clientLastMessage = 0; self.clientChannelOps = []; # }}} + # {{{ dispatchNone(): Dispatch None message from server + def dispatchNone(self): + print("Disconnected from {}:{}.".format(self.serverHname, self.serverPort)) + self.close() + # }}} + # {{{ dispatch001(): Dispatch single 001 (RPL_WELCOME) + def dispatch001(self, message): + print("Registered on {}:{} as {}, {}, {}.".format(self.serverHname, self.serverPort, self.clientNick, self.clientIdent, self.clientGecos)) + print("Joining {} on {}:{}...".format(self.clientChannel, self.serverHname, self.serverPort)) + self.sendline("JOIN", self.clientChannel) + # }}} + # {{{ dispatch353(): Dispatch single 353 (RPL_NAMREPLY) + def dispatch353(self, message): + if message[4].lower() == self.clientChannel.lower(): + for channelNickSpec in message[5].split(" "): + if channelNickSpec[0] == "@" \ + and len(channelNickSpec[1:]): + self.clientChannelOps.append(channelNickSpec[1:].lower()) + print("Authorising {} on {}".format(channelNickSpec[1:].lower(), message[2].lower())) + # }}} + # {{{ dispatchMode(): Dispatch single MODE message from server + def dispatchMode(self, message): + if message[2].lower() == self.clientChannel.lower(): + channelModeType = "+"; channelModeArg = 4; + channelAuthAdd = ""; channelAuthDel = ""; + for channelModeChar in message[3]: + if channelModeChar[0] == "-": + channelModeType = "-" + elif channelModeChar[0] == "+": + channelModeType = "+" + elif channelModeChar[0].isalpha(): + if channelModeChar[0] == "o": + if channelModeType == "+": + channelAuthAdd = message[channelModeArg]; channelAuthDel = ""; + elif channelModeType == "-": + channelAuthAdd = ""; channelAuthDel = message[channelModeArg]; + channelModeArg += 1 + if len(channelAuthAdd) \ + and channelAuthAdd not in self.clientChannelOps: + channelAuthAdd = channelAuthAdd.lower() + print("Authorising {} on {}".format(channelAuthAdd, message[2].lower())) + self.clientChannelOps.append(channelAuthAdd) + elif len(channelAuthDel) \ + and channelAuthDel in self.clientChannelOps: + channelAuthDel = channelAuthDel.lower() + print("Deauthorising {} on {}".format(channelAuthDel, message[2].lower())) + self.clientChannelOps.remove(channelAuthDel) + # }}} + # {{{ dispatchPing(): Dispatch single PING message from server + def dispatchPing(self, message): + self.sendline("PONG", message[2]) + # }}} + # {{{ dispatchPrivmsg(): Dispatch single PRIVMSG message from server + def dispatchPrivmsg(self, message): + if message[2].lower() == self.clientChannel.lower() \ + and message[3].startswith("!pngbot "): + if (int(time.time()) - self.clientLastMessage) < 45: + print("Ignoring request on {} from {} due to rate limit: {}".format(message[2].lower(), message[0], message[3])) + return + elif message[0].split("!")[0].lower() not in self.clientChannelOps: + print("Ignoring request on {} from {} due to lack of authorisation: {}".format(message[2].lower(), message[0], message[3])) + return + else: + print("Processing request on {} from {}: {}".format(message[2].lower(), message[0], message[3])) + self.clientLastMessage = int(time.time()) + asciiUrl = message[3].split(" ")[1] + asciiTmpFilePath = "tmp.txt"; imgTmpFilePath = "tmp.png"; + if os.path.isfile(asciiTmpFilePath): + os.remove(asciiTmpFilePath) + if os.path.isfile(imgTmpFilePath): + os.remove(imgTmpFilePath) + urllib.request.urlretrieve(asciiUrl, asciiTmpFilePath) + _MiRCART = mirc2png.MiRCART(asciiTmpFilePath, imgTmpFilePath, "DejaVuSansMono.ttf", 11) + imgurResponse = self.uploadToImgur(imgTmpFilePath, "MiRCART image", "MiRCART image", "c9a6efb3d7932fd") + if imgurResponse[0] == 200: + print("Uploaded as: {}".format(imgurResponse[1])) + self.sendline("PRIVMSG", message[2], "8/!\\ Uploaded as: {}".format(imgurResponse[1])) + else: + print("Upload failed with HTTP status code {}".format(imgurResponse[0])) + self.sendline("PRIVMSG", message[2], "4/!\\ Uploaded failed with HTTP status code {}!".format(imgurResponse[0])) + if os.path.isfile(asciiTmpFilePath): + os.remove(asciiTmpFilePath) + if os.path.isfile(imgTmpFilePath): + os.remove(imgTmpFilePath) + # }}} # {{{ dispatch(): Read, parse, and dispatch single line from server def dispatch(self): while True: serverMessage = self.readline() if serverMessage == None: - print("Disconnected from {}:{}.".format(self.serverHname, self.serverPort)) - self.close(); break; + self.dispatchNone(); break; elif serverMessage[1] == "001": - print("Registered on {}:{} as {}, {}, {}.".format(self.serverHname, self.serverPort, self.clientNick, self.clientIdent, self.clientGecos)) - print("Joining {} on {}:{}...".format(self.clientChannel, self.serverHname, self.serverPort)) - self.sendline("JOIN", self.clientChannel) - elif serverMessage[1] == "353" \ - and serverMessage[4].lower() == self.clientChannel.lower(): - for channelNickSpec in serverMessage[5].split(" "): - if channelNickSpec[0] == "@" \ - and len(channelNickSpec[1:]): - self.clientChannelOps.append(channelNickSpec[1:].lower()) - print("Authorising {} on {}".format(channelNickSpec[1:].lower(), serverMessage[2].lower())) - elif serverMessage[1] == "MODE" \ - and serverMessage[2].lower() == self.clientChannel.lower(): - channelModeType = "+"; channelModeArg = 4; - channelAuthAdd = ""; channelAuthDel = ""; - for channelModeChar in serverMessage[3]: - if channelModeChar[0] == "-": - channelModeType = "-" - elif channelModeChar[0] == "+": - channelModeType = "+" - elif channelModeChar[0].isalpha(): - if channelModeChar[0] == "o": - if channelModeType == "+": - channelAuthAdd = serverMessage[channelModeArg]; channelAuthDel = ""; - elif channelModeType == "-": - channelAuthAdd = ""; channelAuthDel = serverMessage[channelModeArg]; - channelModeArg += 1 - if len(channelAuthAdd) \ - and channelAuthAdd not in self.clientChannelOps: - channelAuthAdd = channelAuthAdd.lower() - print("Authorising {} on {}".format(channelAuthAdd, serverMessage[2].lower())) - self.clientChannelOps.append(channelAuthAdd) - elif len(channelAuthDel) \ - and channelAuthDel in self.clientChannelOps: - channelAuthDel = channelAuthDel.lower() - print("Deauthorising {} on {}".format(channelAuthDel, serverMessage[2].lower())) - self.clientChannelOps.remove(channelAuthDel) + self.dispatch001(serverMessage) + elif serverMessage[1] == "353": + self.dispatch353(serverMessage) + elif serverMessage[1] == "MODE": + self.dispatchMode(serverMessage) elif serverMessage[1] == "PING": - self.sendline("PONG", serverMessage[2]) - elif serverMessage[1] == "PRIVMSG" \ - and serverMessage[2].lower() == self.clientChannel.lower() \ - and serverMessage[3].startswith("!pngbot "): - if (int(time.time()) - self.clientLastMessage) < 45: - print("Ignoring request on {} from {} due to rate limit: {}".format(serverMessage[2].lower(), serverMessage[0], serverMessage[3])) - continue - elif serverMessage[0].split("!")[0].lower() not in self.clientChannelOps: - print("Ignoring request on {} from {} due to lack of authorisation: {}".format(serverMessage[2].lower(), serverMessage[0], serverMessage[3])) - continue - else: - print("Processing request on {} from {}: {}".format(serverMessage[2].lower(), serverMessage[0], serverMessage[3])) - self.clientLastMessage = int(time.time()) - asciiUrl = serverMessage[3].split(" ")[1] - asciiTmpFilePath = "tmp.txt"; imgTmpFilePath = "tmp.png"; - if os.path.isfile(asciiTmpFilePath): - os.remove(asciiTmpFilePath) - if os.path.isfile(imgTmpFilePath): - os.remove(imgTmpFilePath) - urllib.request.urlretrieve(asciiUrl, asciiTmpFilePath) - _MiRCART = mirc2png.MiRCART(asciiTmpFilePath, imgTmpFilePath, "DejaVuSansMono.ttf", 11) - imgurResponse = self.uploadToImgur(imgTmpFilePath, "MiRCART image", "MiRCART image", "c9a6efb3d7932fd") - if imgurResponse[0] == 200: - print("Uploaded as: {}".format(imgurResponse[1])) - self.sendline("PRIVMSG", serverMessage[2], "8/!\\ Uploaded as: {}".format(imgurResponse[1])) - else: - print("Upload failed with HTTP status code {}".format(imgurResponse[0])) - self.sendline("PRIVMSG", serverMessage[2], "4/!\\ Uploaded failed with HTTP status code {}!".format(imgurResponse[0])) - if os.path.isfile(asciiTmpFilePath): - os.remove(asciiTmpFilePath) - if os.path.isfile(imgTmpFilePath): - os.remove(imgTmpFilePath) + self.dispatchPing(serverMessage) + elif serverMessage[1] == "PRIVMSG": + self.dispatchPrivmsg(serverMessage) # }}} # {{{ uploadToImgur(): Upload single file to Imgur def uploadToImgur(self, imgFilePath, imgName, imgTitle, apiKey): From 8f29ba4e166f4a9e178a5bbeee1cdb40ad53ee87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Tue, 2 Jan 2018 23:38:32 +0100 Subject: [PATCH 019/148] pngbot.py:IrcMiRCARTBot.dispatch353(): correctly print channel name. --- pngbot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pngbot.py b/pngbot.py index 663ac58..9644bc6 100755 --- a/pngbot.py +++ b/pngbot.py @@ -114,7 +114,7 @@ class IrcMiRCARTBot(IrcBot): if channelNickSpec[0] == "@" \ and len(channelNickSpec[1:]): self.clientChannelOps.append(channelNickSpec[1:].lower()) - print("Authorising {} on {}".format(channelNickSpec[1:].lower(), message[2].lower())) + print("Authorising {} on {}".format(channelNickSpec[1:].lower(), message[4].lower())) # }}} # {{{ dispatchMode(): Dispatch single MODE message from server def dispatchMode(self, message): From 95c950d2eebcfbdd1caa62fa75d5f89b53c84d46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Tue, 2 Jan 2018 23:39:38 +0100 Subject: [PATCH 020/148] pngbot.py:IrcMiRCARTBot.dispatch353(): fix indentation. --- pngbot.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pngbot.py b/pngbot.py index 9644bc6..7a129c3 100755 --- a/pngbot.py +++ b/pngbot.py @@ -110,11 +110,11 @@ class IrcMiRCARTBot(IrcBot): # {{{ dispatch353(): Dispatch single 353 (RPL_NAMREPLY) def dispatch353(self, message): if message[4].lower() == self.clientChannel.lower(): - for channelNickSpec in message[5].split(" "): - if channelNickSpec[0] == "@" \ - and len(channelNickSpec[1:]): - self.clientChannelOps.append(channelNickSpec[1:].lower()) - print("Authorising {} on {}".format(channelNickSpec[1:].lower(), message[4].lower())) + for channelNickSpec in message[5].split(" "): + if channelNickSpec[0] == "@" \ + and len(channelNickSpec[1:]): + self.clientChannelOps.append(channelNickSpec[1:].lower()) + print("Authorising {} on {}".format(channelNickSpec[1:].lower(), message[4].lower())) # }}} # {{{ dispatchMode(): Dispatch single MODE message from server def dispatchMode(self, message): From 90d9d146f12c2e735d3a5cc0ef3a8a6df60c093c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Tue, 2 Jan 2018 23:43:39 +0100 Subject: [PATCH 021/148] README.md: update usage information. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2272eba..12d6ca8 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ # mirc2png -- convert ASCII w/ mIRC control codes to monospaced PNG (for EFnet #MiRCART) * Prerequisites: python3 && python3-pil (&& python3-{json,requests,urllib3} for pngbot.py) on Debian-family Linux distributions -* Usage: ./mirc2png.py `` `` [``] [``] +* mirc2png.py usage: mirc2png.py `` `` [``] [``] +* pngbot.py usage: pngbot.py `` [``] [``] [``] [``] [``] From c6aa3f9ad981d812e21586a23ce2aef6cdaabf03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Wed, 3 Jan 2018 00:05:46 +0100 Subject: [PATCH 022/148] pngbot.py:IrcBot.readline(): correctly parse messages w/o a prefix. --- pngbot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pngbot.py b/pngbot.py index 7a129c3..1e72a97 100755 --- a/pngbot.py +++ b/pngbot.py @@ -65,7 +65,7 @@ class IrcBot: if msg[0][0] == ':': msg = [msg[0][1:]] + msg[1:] else: - msg = [""] + msg[1:] + msg = [""] + msg[0:] return msg # }}} # {{{ sendline(): Parse and send single line to server from list From 936bfb6c86d91e02fd4ec9f96aa3d8bf3c03f520 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Wed, 3 Jan 2018 00:13:02 +0100 Subject: [PATCH 023/148] pngbot.py:IrcMiRCARTBot.dispatchKick(): rejoin channel on kick. --- pngbot.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pngbot.py b/pngbot.py index 1e72a97..08d0998 100755 --- a/pngbot.py +++ b/pngbot.py @@ -116,6 +116,13 @@ class IrcMiRCARTBot(IrcBot): self.clientChannelOps.append(channelNickSpec[1:].lower()) print("Authorising {} on {}".format(channelNickSpec[1:].lower(), message[4].lower())) # }}} + # {{{ dispatchKick(): Dispatch single KICK message from server + def dispatchKick(self, message): + if message[2].lower() == self.clientChannel.lower() \ + and message[3].lower() == self.clientNick.lower(): + print("Kicked from {} by {}, rejoining".format(message[2].lower(), message[0])) + self.sendline("JOIN", message[2]) + # }}} # {{{ dispatchMode(): Dispatch single MODE message from server def dispatchMode(self, message): if message[2].lower() == self.clientChannel.lower(): @@ -191,6 +198,8 @@ class IrcMiRCARTBot(IrcBot): self.dispatch001(serverMessage) elif serverMessage[1] == "353": self.dispatch353(serverMessage) + elif serverMessage[1] == "KICK": + self.dispatchKick(serverMessage) elif serverMessage[1] == "MODE": self.dispatchMode(serverMessage) elif serverMessage[1] == "PING": From a0db56a530308cb7e7582394d7022654f7144445 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Wed, 3 Jan 2018 00:40:37 +0100 Subject: [PATCH 024/148] pngbot.py:IrcBot.readline(): add optional timeout parameter. pngbot.py:IrcMiRCARTBot.clientChannelRejoin{,TimerNext}: added to support delayed rejoin-on-kick logic. pngbot.py:IrcMiRCARTBot.dispatchJoin(): reset clientChannelRejoin{,TimerNext} on successful (re)join to clientChannel. pngbot.py:IrcMiRCARTBot.dispatchKick(): set clientChannelRejoin{,TimerNext} on kick from clientChannel to time.time() + 15 (seconds.) pngbot.py:IrcMiRCARTBot.dispatchTimer(): (re)join clientChannel and set clientChannelRejoin{,TimerNext} to time.time() + 15 (seconds.) pngbot.py:IrcMiRCARTBot.dispatch(): call readline() w/ timeout delta given clientChannelRejoinTimerNext. --- pngbot.py | 42 +++++++++++++++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/pngbot.py b/pngbot.py index 08d0998..627e928 100755 --- a/pngbot.py +++ b/pngbot.py @@ -50,9 +50,14 @@ class IrcBot: self.clientSocket.close() self.clientSocket = self.clientSocketFile = None; # }}} - # {{{ readline(): Read and parse single line from server into canonicalised list - def readline(self): - msg = self.clientSocketFile.readline() + # {{{ readline(): Read and parse single line from server into canonicalised list w/ optional timeout + def readline(self, timeout=0): + if timeout: + self.clientSocket.settimeout(timeout) + try: + msg = self.clientSocketFile.readline() + except TimeoutException: + return "TIMEOUT" if len(msg): msg = msg.rstrip("\r\n") else: @@ -87,6 +92,7 @@ class IrcBot: class IrcMiRCARTBot(IrcBot): """IRC<->MiRCART bot""" clientChannelLastMessage = clientChannelOps = clientChannel = None + clientChannelRejoinTimerNext = clientChannelRejoin = None # {{{ connect(): Connect to server and (re)initialise def connect(self): @@ -95,6 +101,7 @@ class IrcMiRCARTBot(IrcBot): print("Connected to {}:{}.".format(self.serverHname, self.serverPort)) print("Registering on {}:{} as {}, {}, {}...".format(self.serverHname, self.serverPort, self.clientNick, self.clientIdent, self.clientGecos)) self.clientLastMessage = 0; self.clientChannelOps = []; + clientChannelRejoinTimerNext = 0; self.clientChannelRejoin = False; # }}} # {{{ dispatchNone(): Dispatch None message from server def dispatchNone(self): @@ -104,7 +111,7 @@ class IrcMiRCARTBot(IrcBot): # {{{ dispatch001(): Dispatch single 001 (RPL_WELCOME) def dispatch001(self, message): print("Registered on {}:{} as {}, {}, {}.".format(self.serverHname, self.serverPort, self.clientNick, self.clientIdent, self.clientGecos)) - print("Joining {} on {}:{}...".format(self.clientChannel, self.serverHname, self.serverPort)) + print("Attempting to join {} on {}:{}...".format(self.clientChannel, self.serverHname, self.serverPort)) self.sendline("JOIN", self.clientChannel) # }}} # {{{ dispatch353(): Dispatch single 353 (RPL_NAMREPLY) @@ -116,12 +123,17 @@ class IrcMiRCARTBot(IrcBot): self.clientChannelOps.append(channelNickSpec[1:].lower()) print("Authorising {} on {}".format(channelNickSpec[1:].lower(), message[4].lower())) # }}} + # {{{ dispatchJoin(): Dispatch single JOIN message from server + def dispatchJoin(self, message): + print("Joined {} on {}:{}.".format(message[2].lower(), self.serverHname, self.serverPort)) + self.clientChannelRejoinTimerNext = 0; self.clientChannelRejoin = False; + # }}} # {{{ dispatchKick(): Dispatch single KICK message from server def dispatchKick(self, message): if message[2].lower() == self.clientChannel.lower() \ and message[3].lower() == self.clientNick.lower(): - print("Kicked from {} by {}, rejoining".format(message[2].lower(), message[0])) - self.sendline("JOIN", message[2]) + print("Kicked from {} by {}, rejoining in 15 seconds".format(message[2].lower(), message[0])) + self.clientChannelRejoinTimerNext = time.time() + 15; self.clientChannelRejoin = True; # }}} # {{{ dispatchMode(): Dispatch single MODE message from server def dispatchMode(self, message): @@ -188,16 +200,32 @@ class IrcMiRCARTBot(IrcBot): if os.path.isfile(imgTmpFilePath): os.remove(imgTmpFilePath) # }}} + # {{{ dispatchTimer(): Dispatch single client timer expiration + def dispatchTimer(self): + if self.clientChannelRejoin: + print("Attempting to join {} on {}:{}...".format(self.clientChannel, self.serverHname, self.serverPort)) + self.sendline("JOIN", self.clientChannel) + self.clientChannelRejoinTimerNext = time.time() + 15; self.clientChannelRejoin = True; + # }}} # {{{ dispatch(): Read, parse, and dispatch single line from server def dispatch(self): while True: - serverMessage = self.readline() + clientTimerNextDelta = 0 + if self.clientChannelRejoinTimerNext: + timeNow = time.time() + if self.clientChannelRejoinTimerNext > timeNow: + clientTimerNextDelta = self.clientChannelRejoinTimerNext - timeNow + serverMessage = self.readline(clientTimerNextDelta) if serverMessage == None: self.dispatchNone(); break; + elif serverMessage == "TIMEOUT": + self.dispatchTimer() elif serverMessage[1] == "001": self.dispatch001(serverMessage) elif serverMessage[1] == "353": self.dispatch353(serverMessage) + elif serverMessage[1] == "JOIN": + self.dispatchJoin(serverMessage) elif serverMessage[1] == "KICK": self.dispatchKick(serverMessage) elif serverMessage[1] == "MODE": From ebd2ce93936d9f96a4366cdc284607e58a507e5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Wed, 3 Jan 2018 00:53:42 +0100 Subject: [PATCH 025/148] pngbot.py:IrcMiRCARTBot.dispatch353(): skip empty nick specs. --- pngbot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pngbot.py b/pngbot.py index 627e928..fdfa397 100755 --- a/pngbot.py +++ b/pngbot.py @@ -118,7 +118,8 @@ class IrcMiRCARTBot(IrcBot): def dispatch353(self, message): if message[4].lower() == self.clientChannel.lower(): for channelNickSpec in message[5].split(" "): - if channelNickSpec[0] == "@" \ + if len(channelNickSpec) \ + and channelNickSpec[0] == "@" \ and len(channelNickSpec[1:]): self.clientChannelOps.append(channelNickSpec[1:].lower()) print("Authorising {} on {}".format(channelNickSpec[1:].lower(), message[4].lower())) From 532806176cdfb97a476b1cbadc218002758f7cfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Wed, 3 Jan 2018 00:57:55 +0100 Subject: [PATCH 026/148] pngbot.py:IrcBot.readline(): correctly catch socket.timeout exception. --- pngbot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pngbot.py b/pngbot.py index fdfa397..dac750e 100755 --- a/pngbot.py +++ b/pngbot.py @@ -56,7 +56,7 @@ class IrcBot: self.clientSocket.settimeout(timeout) try: msg = self.clientSocketFile.readline() - except TimeoutException: + except socket.timeout: return "TIMEOUT" if len(msg): msg = msg.rstrip("\r\n") From 09a7995a50789d953df6aedca4f32d4498605626 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Wed, 3 Jan 2018 01:25:46 +0100 Subject: [PATCH 027/148] mirc2png.py:MiRCART.parseAsChar(): implement ^V (reverse) by swapping {background,foreground} colours. --- mirc2png.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/mirc2png.py b/mirc2png.py index d88ce8b..a3c34f2 100755 --- a/mirc2png.py +++ b/mirc2png.py @@ -36,7 +36,7 @@ class MiRCART: outCurColourBg = outCurColourFg = None; outCurX = outCurY = None; - inCurBold = inCurItalic = inCurReverse = inCurUnderline = None; + inCurBold = inCurItalic = inCurUnderline = None; inCurColourSpec = None; state = None; inCurCol = None; @@ -120,10 +120,11 @@ class MiRCART: self.inCurCol += 1; self.inCurItalic = 0 if self.inCurItalic else 1; elif char == "": self.inCurCol += 1; - self.inCurBold = 0; self.inCurItalic = 0; self.inCurReverse = 0; self.inCurUnderline = 0; + self.inCurBold = 0; self.inCurItalic = 0; self.inCurUnderline = 0; self.inCurColourSpec = ""; elif char == "": - self.inCurCol += 1; self.inCurReverse = 0 if self.inCurReverse else 1; + self.inCurCol += 1 + self.outCurColourBg, self.outCurColourFg = self.outCurColourFg, self.outCurColourBg; elif char == "": self.inCurCol += 1; self.inCurUnderline = 0 if self.inCurUnderline else 1; elif char == " ": @@ -143,7 +144,7 @@ class MiRCART: colourBg = self.ColourMapNormal[self.outCurColourBg] colourFg = self.ColourMapNormal[self.outCurColourFg] self.outImgDraw.rectangle(((self.outCurX, self.outCurY), (self.outCurX + 7, self.outCurY + 14)), fill=colourBg) - # XXX implement italic, reverse + # XXX implement italic self.outImgDraw.text((self.outCurX, self.outCurY), char, colourFg, self.outImgFont) if self.inCurUnderline: self.outImgDraw.line((self.outCurX, self.outCurY + 11, self.outCurX + 7, self.outCurY + 11), fill=colourFg) @@ -177,7 +178,7 @@ class MiRCART: self.outCurColourBg = 1; self.outCurColourFg = 15; self.outCurX = 0; self.outCurY = 0; for inCurRow in range(0, len(self.inLines)): - self.inCurBold = 0; self.inCurItalic = 0; self.inCurReverse = 0; self.inCurUnderline = 0; + self.inCurBold = 0; self.inCurItalic = 0; self.inCurUnderline = 0; self.inCurColourSpec = ""; self.state = self.State.STATE_CHAR; self.inCurCol = 0; while self.inCurCol < len(self.inLines[inCurRow]): From 3a207fc53c5f07800463aafe93dc937afa1612e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Wed, 3 Jan 2018 01:38:41 +0100 Subject: [PATCH 028/148] pngbot.py:IrcBot.connect(): set socket non-blocking, ignore BlockingIOError, and select() for writability & optionally specified timeout. pngbot.py:IrcBot.readline(): replace settimeout()/socket.timeout-based logic w/ select() for readability & optionally specified timeout. pngbot.py:IrcBot.sendline(): select() for writability & optionally specified timeout and send() full buffer until failure. pngbot.py:IrcMiRCARTBot.dispatch(): dispatch expired timers prior to calling readline(). pngbot.py:main(): default (maximum) connect timeout to 15 seconds. --- pngbot.py | 61 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 39 insertions(+), 22 deletions(-) diff --git a/pngbot.py b/pngbot.py index dac750e..f00954c 100755 --- a/pngbot.py +++ b/pngbot.py @@ -24,22 +24,29 @@ from itertools import chain import base64 +import errno, os, select, socket, sys, time import json import mirc2png -import os, socket, sys, time -import requests -import urllib.request +import requests, urllib.request class IrcBot: - """Blocking abstraction over the IRC protocol""" + """Non-blocking abstraction over the IRC protocol""" serverHname = serverPort = None; clientNick = clientIdent = clientGecos = None; clientSocket = clientSocketFile = None; - # {{{ connect(): Connect to server and register - def connect(self): + # {{{ connect(): Connect to server and register w/ optional timeout + def connect(self, timeout=None): self.clientSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.clientSocket.connect((self.serverHname, int(self.serverPort))) + self.clientSocket.setblocking(0) + try: + self.clientSocket.connect((self.serverHname, int(self.serverPort))) + except BlockingIOError: + pass + if timeout: + select.select([], [self.clientSocket.fileno()], [], timeout) + else: + select.select([], [self.clientSocket.fileno()], []) self.clientSocketFile = self.clientSocket.makefile() self.sendline("NICK", self.clientNick) self.sendline("USER", self.clientIdent, "0", "0", self.clientGecos) @@ -51,13 +58,12 @@ class IrcBot: self.clientSocket = self.clientSocketFile = None; # }}} # {{{ readline(): Read and parse single line from server into canonicalised list w/ optional timeout - def readline(self, timeout=0): + def readline(self, timeout=None): if timeout: - self.clientSocket.settimeout(timeout) - try: - msg = self.clientSocketFile.readline() - except socket.timeout: - return "TIMEOUT" + select.select([self.clientSocket.fileno()], [], [], timeout) + else: + select.select([self.clientSocket.fileno()], [], []) + msg = self.clientSocketFile.readline() if len(msg): msg = msg.rstrip("\r\n") else: @@ -73,15 +79,24 @@ class IrcBot: msg = [""] + msg[0:] return msg # }}} - # {{{ sendline(): Parse and send single line to server from list - def sendline(self, *args): + # {{{ sendline(): Parse and send single line to server from list w/ optional timeout + def sendline(self, *args, timeout=None): msg = ""; argNumMax = len(args); for argNum in range(0, argNumMax): if argNum == (argNumMax - 1): msg += ":" + args[argNum] else: msg += args[argNum] + " " - return self.clientSocket.send((msg + "\r\n").encode()) + msg = (msg + "\r\n").encode(); msgLen = len(msg); msgBytesSent = 0; + while msgBytesSent < msgLen: + if timeout: + timeLast = time.time() + select.select([], [self.clientSocket.fileno()], [], timeout) + timeNow = time.time(); timeout -= timeNow - timeLast; + else: + select.select([], [self.clientSocket.fileno()], []) + msgBytesSent = self.clientSocket.send(msg) + msg = msg[msgBytesSent:]; msgLen -= msgBytesSent; # }}} # {{{ Initialisation method def __init__(self, serverHname, serverPort, clientNick, clientIdent, clientGecos): @@ -94,10 +109,10 @@ class IrcMiRCARTBot(IrcBot): clientChannelLastMessage = clientChannelOps = clientChannel = None clientChannelRejoinTimerNext = clientChannelRejoin = None - # {{{ connect(): Connect to server and (re)initialise - def connect(self): + # {{{ connect(): Connect to server and (re)initialise w/ optional timeout + def connect(self, timeout=None): print("Connecting to {}:{}...".format(self.serverHname, self.serverPort)) - super().connect() + super().connect(timeout) print("Connected to {}:{}.".format(self.serverHname, self.serverPort)) print("Registering on {}:{} as {}, {}, {}...".format(self.serverHname, self.serverPort, self.clientNick, self.clientIdent, self.clientGecos)) self.clientLastMessage = 0; self.clientChannelOps = []; @@ -216,11 +231,13 @@ class IrcMiRCARTBot(IrcBot): timeNow = time.time() if self.clientChannelRejoinTimerNext > timeNow: clientTimerNextDelta = self.clientChannelRejoinTimerNext - timeNow + if clientTimerNextDelta: + timeNow = time.time() + if self.clientChannelRejoinTimerNext <= timeNow: + self.dispatchTimer() serverMessage = self.readline(clientTimerNextDelta) if serverMessage == None: self.dispatchNone(); break; - elif serverMessage == "TIMEOUT": - self.dispatchTimer() elif serverMessage[1] == "001": self.dispatch001(serverMessage) elif serverMessage[1] == "353": @@ -265,7 +282,7 @@ class IrcMiRCARTBot(IrcBot): def main(*argv): _IrcMiRCARTBot = IrcMiRCARTBot(*argv[1:]) while True: - _IrcMiRCARTBot.connect() + _IrcMiRCARTBot.connect(15) _IrcMiRCARTBot.dispatch() _IrcMiRCARTBot.close() From 98eae4257c2aefe47737938747e64ded50508815 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Wed, 3 Jan 2018 02:16:55 +0100 Subject: [PATCH 029/148] pngbot.py:IrcBot.clientNextTimeout: moved from IrcMiRCARTBot. pngbot.py:IrcBot.connect(): close() & return False given timeout and True given success. pngbot.py:IrcBot.readline(): honour clientNextTimeout and return "" given timer expiry. pngbot.py:IrcBot.sendline(): ignore clientNextTimeout. pngbot.py:IrcMiRCARTBot.connect(): honour & pass IrcBot.connect() return value. pngbot.py:IrcMiRCARTBot.dispatch{Join,Kick,Timer}(): (re)set clientNextTimeout to time.time() + 15 seconds. pngbot.py:IrcMiRCARTBot.dispatch(): dispatch expired timers prior to calling readline() & honour "" return value. pngbot.py:main(): honour IrcMiRCARTBot.connect() return value & delay reconnection by 15 seconds. --- pngbot.py | 77 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 42 insertions(+), 35 deletions(-) diff --git a/pngbot.py b/pngbot.py index f00954c..ca701a6 100755 --- a/pngbot.py +++ b/pngbot.py @@ -34,6 +34,7 @@ class IrcBot: serverHname = serverPort = None; clientNick = clientIdent = clientGecos = None; clientSocket = clientSocketFile = None; + clientNextTimeout = None # {{{ connect(): Connect to server and register w/ optional timeout def connect(self, timeout=None): @@ -44,12 +45,15 @@ class IrcBot: except BlockingIOError: pass if timeout: - select.select([], [self.clientSocket.fileno()], [], timeout) + readySet = select.select([], [self.clientSocket.fileno()], [], timeout) + if len(readySet[1]) == 0: + self.close(); return False; else: select.select([], [self.clientSocket.fileno()], []) self.clientSocketFile = self.clientSocket.makefile() self.sendline("NICK", self.clientNick) self.sendline("USER", self.clientIdent, "0", "0", self.clientGecos) + return True # }}} # {{{ close(): Close connection to server def close(self): @@ -57,17 +61,24 @@ class IrcBot: self.clientSocket.close() self.clientSocket = self.clientSocketFile = None; # }}} - # {{{ readline(): Read and parse single line from server into canonicalised list w/ optional timeout - def readline(self, timeout=None): - if timeout: - select.select([self.clientSocket.fileno()], [], [], timeout) + # {{{ readline(): Read and parse single line from server into canonicalised list, honouring timers + def readline(self): + if self.clientNextTimeout: + timeNow = time.time() + if self.clientNextTimeout <= timeNow: + return "" + else: + readySet = select.select([self.clientSocket.fileno()], [], [], self.clientNextTimeout - timeNow) else: - select.select([self.clientSocket.fileno()], [], []) + readySet = select.select([self.clientSocket.fileno()], [], []) msg = self.clientSocketFile.readline() if len(msg): msg = msg.rstrip("\r\n") else: - return None + if len(readySet[0]) == 0: + return "" + else: + return None msg = msg.split(" :", 1) if len(msg) == 1: msg = list(chain.from_iterable(m.split(" ") for m in msg)) @@ -79,8 +90,8 @@ class IrcBot: msg = [""] + msg[0:] return msg # }}} - # {{{ sendline(): Parse and send single line to server from list w/ optional timeout - def sendline(self, *args, timeout=None): + # {{{ sendline(): Parse and send single line to server from list, ignoring timers + def sendline(self, *args): msg = ""; argNumMax = len(args); for argNum in range(0, argNumMax): if argNum == (argNumMax - 1): @@ -89,12 +100,7 @@ class IrcBot: msg += args[argNum] + " " msg = (msg + "\r\n").encode(); msgLen = len(msg); msgBytesSent = 0; while msgBytesSent < msgLen: - if timeout: - timeLast = time.time() - select.select([], [self.clientSocket.fileno()], [], timeout) - timeNow = time.time(); timeout -= timeNow - timeLast; - else: - select.select([], [self.clientSocket.fileno()], []) + readySet = select.select([], [self.clientSocket.fileno()], []) msgBytesSent = self.clientSocket.send(msg) msg = msg[msgBytesSent:]; msgLen -= msgBytesSent; # }}} @@ -107,16 +113,19 @@ class IrcBot: class IrcMiRCARTBot(IrcBot): """IRC<->MiRCART bot""" clientChannelLastMessage = clientChannelOps = clientChannel = None - clientChannelRejoinTimerNext = clientChannelRejoin = None + clientChannelRejoin = None # {{{ connect(): Connect to server and (re)initialise w/ optional timeout def connect(self, timeout=None): print("Connecting to {}:{}...".format(self.serverHname, self.serverPort)) - super().connect(timeout) - print("Connected to {}:{}.".format(self.serverHname, self.serverPort)) - print("Registering on {}:{} as {}, {}, {}...".format(self.serverHname, self.serverPort, self.clientNick, self.clientIdent, self.clientGecos)) - self.clientLastMessage = 0; self.clientChannelOps = []; - clientChannelRejoinTimerNext = 0; self.clientChannelRejoin = False; + if super().connect(timeout): + print("Connected to {}:{}.".format(self.serverHname, self.serverPort)) + print("Registering on {}:{} as {}, {}, {}...".format(self.serverHname, self.serverPort, self.clientNick, self.clientIdent, self.clientGecos)) + self.clientLastMessage = 0; self.clientChannelOps = []; + self.clientChannelRejoin = False + return True + else: + return False # }}} # {{{ dispatchNone(): Dispatch None message from server def dispatchNone(self): @@ -142,14 +151,14 @@ class IrcMiRCARTBot(IrcBot): # {{{ dispatchJoin(): Dispatch single JOIN message from server def dispatchJoin(self, message): print("Joined {} on {}:{}.".format(message[2].lower(), self.serverHname, self.serverPort)) - self.clientChannelRejoinTimerNext = 0; self.clientChannelRejoin = False; + self.clientNextTimeout = None; self.clientChannelRejoin = False; # }}} # {{{ dispatchKick(): Dispatch single KICK message from server def dispatchKick(self, message): if message[2].lower() == self.clientChannel.lower() \ and message[3].lower() == self.clientNick.lower(): print("Kicked from {} by {}, rejoining in 15 seconds".format(message[2].lower(), message[0])) - self.clientChannelRejoinTimerNext = time.time() + 15; self.clientChannelRejoin = True; + self.clientNextTimeout = time.time() + 15; self.clientChannelRejoin = True; # }}} # {{{ dispatchMode(): Dispatch single MODE message from server def dispatchMode(self, message): @@ -221,23 +230,20 @@ class IrcMiRCARTBot(IrcBot): if self.clientChannelRejoin: print("Attempting to join {} on {}:{}...".format(self.clientChannel, self.serverHname, self.serverPort)) self.sendline("JOIN", self.clientChannel) - self.clientChannelRejoinTimerNext = time.time() + 15; self.clientChannelRejoin = True; + self.clientNextTimeout = time.time() + 15; self.clientChannelRejoin = True; # }}} # {{{ dispatch(): Read, parse, and dispatch single line from server def dispatch(self): while True: - clientTimerNextDelta = 0 - if self.clientChannelRejoinTimerNext: + if self.clientNextTimeout: timeNow = time.time() - if self.clientChannelRejoinTimerNext > timeNow: - clientTimerNextDelta = self.clientChannelRejoinTimerNext - timeNow - if clientTimerNextDelta: - timeNow = time.time() - if self.clientChannelRejoinTimerNext <= timeNow: + if self.clientNextTimeout <= timeNow: self.dispatchTimer() - serverMessage = self.readline(clientTimerNextDelta) + serverMessage = self.readline() if serverMessage == None: self.dispatchNone(); break; + elif serverMessage == "": + continue elif serverMessage[1] == "001": self.dispatch001(serverMessage) elif serverMessage[1] == "353": @@ -282,9 +288,10 @@ class IrcMiRCARTBot(IrcBot): def main(*argv): _IrcMiRCARTBot = IrcMiRCARTBot(*argv[1:]) while True: - _IrcMiRCARTBot.connect(15) - _IrcMiRCARTBot.dispatch() - _IrcMiRCARTBot.close() + if _IrcMiRCARTBot.connect(15): + _IrcMiRCARTBot.dispatch() + _IrcMiRCARTBot.close() + time.sleep(15) if __name__ == "__main__": if ((len(sys.argv) - 1) < 1)\ From c32d749675f3ddeb2e39f0ae94a997329f7566b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Wed, 3 Jan 2018 02:33:12 +0100 Subject: [PATCH 030/148] IrcClient.py: split from IrcMiRCARTBot.py. IrcMiRCARTBot.py: renamed from pngbot.py, importing IrcClient.IrcClient. MiRCART.py: renamed from mirc2png.py. README.md: updated. --- IrcClient.py | 110 ++++++++++++++++++++++++++++++++++ pngbot.py => IrcMiRCARTBot.py | 93 ++-------------------------- mirc2png.py => MiRCART.py | 5 +- README.md | 6 +- 4 files changed, 122 insertions(+), 92 deletions(-) create mode 100644 IrcClient.py rename pngbot.py => IrcMiRCARTBot.py (77%) rename mirc2png.py => MiRCART.py (99%) diff --git a/IrcClient.py b/IrcClient.py new file mode 100644 index 0000000..b8eccb9 --- /dev/null +++ b/IrcClient.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +# +# mirc2png -- convert ASCII w/ mIRC control codes to monospaced PNG (for EFnet #MiRCART) +# Copyright (c) 2018 Lucio Andrés Illanes Albornoz +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +from itertools import chain +import select, socket, time + +class IrcClient: + """Non-blocking abstraction over the IRC protocol""" + serverHname = serverPort = None; + clientNick = clientIdent = clientGecos = None; + clientSocket = clientSocketFile = None; + clientNextTimeout = None + + # {{{ connect(): Connect to server and register w/ optional timeout + def connect(self, timeout=None): + self.clientSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.clientSocket.setblocking(0) + try: + self.clientSocket.connect((self.serverHname, int(self.serverPort))) + except BlockingIOError: + pass + if timeout: + readySet = select.select([], [self.clientSocket.fileno()], [], timeout) + if len(readySet[1]) == 0: + self.close(); return False; + else: + select.select([], [self.clientSocket.fileno()], []) + self.clientSocketFile = self.clientSocket.makefile() + self.sendline("NICK", self.clientNick) + self.sendline("USER", self.clientIdent, "0", "0", self.clientGecos) + return True + # }}} + # {{{ close(): Close connection to server + def close(self): + if self.clientSocket != None: + self.clientSocket.close() + self.clientSocket = self.clientSocketFile = None; + # }}} + # {{{ readline(): Read and parse single line from server into canonicalised list, honouring timers + def readline(self): + if self.clientNextTimeout: + timeNow = time.time() + if self.clientNextTimeout <= timeNow: + return "" + else: + readySet = select.select([self.clientSocket.fileno()], [], [], self.clientNextTimeout - timeNow) + else: + readySet = select.select([self.clientSocket.fileno()], [], []) + msg = self.clientSocketFile.readline() + if len(msg): + msg = msg.rstrip("\r\n") + else: + if len(readySet[0]) == 0: + return "" + else: + return None + msg = msg.split(" :", 1) + if len(msg) == 1: + msg = list(chain.from_iterable(m.split(" ") for m in msg)) + elif len(msg) == 2: + msg = msg[0].split(" ") + [msg[1]] + if msg[0][0] == ':': + msg = [msg[0][1:]] + msg[1:] + else: + msg = [""] + msg[0:] + return msg + # }}} + # {{{ sendline(): Parse and send single line to server from list, ignoring timers + def sendline(self, *args): + msg = ""; argNumMax = len(args); + for argNum in range(0, argNumMax): + if argNum == (argNumMax - 1): + msg += ":" + args[argNum] + else: + msg += args[argNum] + " " + msg = (msg + "\r\n").encode(); msgLen = len(msg); msgBytesSent = 0; + while msgBytesSent < msgLen: + readySet = select.select([], [self.clientSocket.fileno()], []) + msgBytesSent = self.clientSocket.send(msg) + msg = msg[msgBytesSent:]; msgLen -= msgBytesSent; + # }}} + + # + # Initialisation method + def __init__(self, serverHname, serverPort, clientNick, clientIdent, clientGecos): + self.serverHname = serverHname; self.serverPort = serverPort; + self.clientNick = clientNick; self.clientIdent = clientIdent; self.clientGecos = clientGecos; + +# vim:expandtab foldmethod=marker sw=8 ts=8 tw=120 diff --git a/pngbot.py b/IrcMiRCARTBot.py similarity index 77% rename from pngbot.py rename to IrcMiRCARTBot.py index ca701a6..5ab59ba 100755 --- a/pngbot.py +++ b/IrcMiRCARTBot.py @@ -22,95 +22,13 @@ # SOFTWARE. # -from itertools import chain import base64 -import errno, os, select, socket, sys, time +import os, sys, time import json -import mirc2png +import IrcClient, MiRCART import requests, urllib.request -class IrcBot: - """Non-blocking abstraction over the IRC protocol""" - serverHname = serverPort = None; - clientNick = clientIdent = clientGecos = None; - clientSocket = clientSocketFile = None; - clientNextTimeout = None - - # {{{ connect(): Connect to server and register w/ optional timeout - def connect(self, timeout=None): - self.clientSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.clientSocket.setblocking(0) - try: - self.clientSocket.connect((self.serverHname, int(self.serverPort))) - except BlockingIOError: - pass - if timeout: - readySet = select.select([], [self.clientSocket.fileno()], [], timeout) - if len(readySet[1]) == 0: - self.close(); return False; - else: - select.select([], [self.clientSocket.fileno()], []) - self.clientSocketFile = self.clientSocket.makefile() - self.sendline("NICK", self.clientNick) - self.sendline("USER", self.clientIdent, "0", "0", self.clientGecos) - return True - # }}} - # {{{ close(): Close connection to server - def close(self): - if self.clientSocket != None: - self.clientSocket.close() - self.clientSocket = self.clientSocketFile = None; - # }}} - # {{{ readline(): Read and parse single line from server into canonicalised list, honouring timers - def readline(self): - if self.clientNextTimeout: - timeNow = time.time() - if self.clientNextTimeout <= timeNow: - return "" - else: - readySet = select.select([self.clientSocket.fileno()], [], [], self.clientNextTimeout - timeNow) - else: - readySet = select.select([self.clientSocket.fileno()], [], []) - msg = self.clientSocketFile.readline() - if len(msg): - msg = msg.rstrip("\r\n") - else: - if len(readySet[0]) == 0: - return "" - else: - return None - msg = msg.split(" :", 1) - if len(msg) == 1: - msg = list(chain.from_iterable(m.split(" ") for m in msg)) - elif len(msg) == 2: - msg = msg[0].split(" ") + [msg[1]] - if msg[0][0] == ':': - msg = [msg[0][1:]] + msg[1:] - else: - msg = [""] + msg[0:] - return msg - # }}} - # {{{ sendline(): Parse and send single line to server from list, ignoring timers - def sendline(self, *args): - msg = ""; argNumMax = len(args); - for argNum in range(0, argNumMax): - if argNum == (argNumMax - 1): - msg += ":" + args[argNum] - else: - msg += args[argNum] + " " - msg = (msg + "\r\n").encode(); msgLen = len(msg); msgBytesSent = 0; - while msgBytesSent < msgLen: - readySet = select.select([], [self.clientSocket.fileno()], []) - msgBytesSent = self.clientSocket.send(msg) - msg = msg[msgBytesSent:]; msgLen -= msgBytesSent; - # }}} - # {{{ Initialisation method - def __init__(self, serverHname, serverPort, clientNick, clientIdent, clientGecos): - self.serverHname = serverHname; self.serverPort = serverPort; - self.clientNick = clientNick; self.clientIdent = clientIdent; self.clientGecos = clientGecos; - # }}} - -class IrcMiRCARTBot(IrcBot): +class IrcMiRCARTBot(IrcClient.IrcClient): """IRC<->MiRCART bot""" clientChannelLastMessage = clientChannelOps = clientChannel = None clientChannelRejoin = None @@ -277,11 +195,12 @@ class IrcMiRCARTBot(IrcBot): else: return [responseHttp.status_code] # }}} - # {{{ Initialisation method + + # + # Initialisation method def __init__(self, serverHname, serverPort="6667", clientNick="pngbot", clientIdent="pngbot", clientGecos="pngbot", clientChannel="#MiRCART"): super().__init__(serverHname, serverPort, clientNick, clientIdent, clientGecos) self.clientChannel = clientChannel - # }}} # # Entry point diff --git a/mirc2png.py b/MiRCART.py similarity index 99% rename from mirc2png.py rename to MiRCART.py index a3c34f2..9fac4cb 100755 --- a/mirc2png.py +++ b/MiRCART.py @@ -165,7 +165,9 @@ class MiRCART: self.outCurColourBg = 1; self.outCurColourFg = 15; self.inCurColourSpec = ""; self.state = self.State.STATE_CHAR; # }}} - # {{{ Initialisation method + + # + # Initialisation method def __init__(self, inFilePath, imgFilePath, fontFilePath="DejaVuSansMono.ttf", fontSize=11): self.inFilePath = inFilePath; self.inFile = open(inFilePath, "r"); self.inLines = self.inFile.readlines() @@ -189,7 +191,6 @@ class MiRCART: self.outCurX = 0; self.outCurY += 13; self.inFile.close(); self.outImg.save(imgFilePath); - # }}} # # Entry point diff --git a/README.md b/README.md index 12d6ca8..6239374 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ # mirc2png -- convert ASCII w/ mIRC control codes to monospaced PNG (for EFnet #MiRCART) -* Prerequisites: python3 && python3-pil (&& python3-{json,requests,urllib3} for pngbot.py) on Debian-family Linux distributions -* mirc2png.py usage: mirc2png.py `` `` [``] [``] -* pngbot.py usage: pngbot.py `` [``] [``] [``] [``] [``] +* Prerequisites: python3 && python3-pil (&& python3-{json,requests,urllib3} for IrcMiRCARTBot.py) on Debian-family Linux distributions +* IrcMiRCARTBot.py usage: IrcMiRCARTBot.py `` [``] [``] [``] [``] [``] +* MiRCART.py usage: MiRCART.py `` `` [``] [``] From 9896108028c4904f47ded48a94270bc05a4076dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Wed, 3 Jan 2018 03:28:05 +0100 Subject: [PATCH 031/148] IrcMiRCARTBot.py:IrcMiRCARTBot: replace print() w/ _log() calls. IrcMiRCARTBot.py:IrcMiRCARTBot._log(): log message w/ timestamp. --- IrcMiRCARTBot.py | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/IrcMiRCARTBot.py b/IrcMiRCARTBot.py index 5ab59ba..a2d1a50 100755 --- a/IrcMiRCARTBot.py +++ b/IrcMiRCARTBot.py @@ -35,10 +35,10 @@ class IrcMiRCARTBot(IrcClient.IrcClient): # {{{ connect(): Connect to server and (re)initialise w/ optional timeout def connect(self, timeout=None): - print("Connecting to {}:{}...".format(self.serverHname, self.serverPort)) + self._log("Connecting to {}:{}...".format(self.serverHname, self.serverPort)) if super().connect(timeout): - print("Connected to {}:{}.".format(self.serverHname, self.serverPort)) - print("Registering on {}:{} as {}, {}, {}...".format(self.serverHname, self.serverPort, self.clientNick, self.clientIdent, self.clientGecos)) + self._log("Connected to {}:{}.".format(self.serverHname, self.serverPort)) + self._log("Registering on {}:{} as {}, {}, {}...".format(self.serverHname, self.serverPort, self.clientNick, self.clientIdent, self.clientGecos)) self.clientLastMessage = 0; self.clientChannelOps = []; self.clientChannelRejoin = False return True @@ -47,13 +47,13 @@ class IrcMiRCARTBot(IrcClient.IrcClient): # }}} # {{{ dispatchNone(): Dispatch None message from server def dispatchNone(self): - print("Disconnected from {}:{}.".format(self.serverHname, self.serverPort)) + self._log("Disconnected from {}:{}.".format(self.serverHname, self.serverPort)) self.close() # }}} # {{{ dispatch001(): Dispatch single 001 (RPL_WELCOME) def dispatch001(self, message): - print("Registered on {}:{} as {}, {}, {}.".format(self.serverHname, self.serverPort, self.clientNick, self.clientIdent, self.clientGecos)) - print("Attempting to join {} on {}:{}...".format(self.clientChannel, self.serverHname, self.serverPort)) + self._log("Registered on {}:{} as {}, {}, {}.".format(self.serverHname, self.serverPort, self.clientNick, self.clientIdent, self.clientGecos)) + self._log("Attempting to join {} on {}:{}...".format(self.clientChannel, self.serverHname, self.serverPort)) self.sendline("JOIN", self.clientChannel) # }}} # {{{ dispatch353(): Dispatch single 353 (RPL_NAMREPLY) @@ -64,18 +64,18 @@ class IrcMiRCARTBot(IrcClient.IrcClient): and channelNickSpec[0] == "@" \ and len(channelNickSpec[1:]): self.clientChannelOps.append(channelNickSpec[1:].lower()) - print("Authorising {} on {}".format(channelNickSpec[1:].lower(), message[4].lower())) + self._log("Authorising {} on {}".format(channelNickSpec[1:].lower(), message[4].lower())) # }}} # {{{ dispatchJoin(): Dispatch single JOIN message from server def dispatchJoin(self, message): - print("Joined {} on {}:{}.".format(message[2].lower(), self.serverHname, self.serverPort)) + self._log("Joined {} on {}:{}.".format(message[2].lower(), self.serverHname, self.serverPort)) self.clientNextTimeout = None; self.clientChannelRejoin = False; # }}} # {{{ dispatchKick(): Dispatch single KICK message from server def dispatchKick(self, message): if message[2].lower() == self.clientChannel.lower() \ and message[3].lower() == self.clientNick.lower(): - print("Kicked from {} by {}, rejoining in 15 seconds".format(message[2].lower(), message[0])) + self._log("Kicked from {} by {}, rejoining in 15 seconds".format(message[2].lower(), message[0])) self.clientNextTimeout = time.time() + 15; self.clientChannelRejoin = True; # }}} # {{{ dispatchMode(): Dispatch single MODE message from server @@ -98,12 +98,12 @@ class IrcMiRCARTBot(IrcClient.IrcClient): if len(channelAuthAdd) \ and channelAuthAdd not in self.clientChannelOps: channelAuthAdd = channelAuthAdd.lower() - print("Authorising {} on {}".format(channelAuthAdd, message[2].lower())) + self._log("Authorising {} on {}".format(channelAuthAdd, message[2].lower())) self.clientChannelOps.append(channelAuthAdd) elif len(channelAuthDel) \ and channelAuthDel in self.clientChannelOps: channelAuthDel = channelAuthDel.lower() - print("Deauthorising {} on {}".format(channelAuthDel, message[2].lower())) + self._log("Deauthorising {} on {}".format(channelAuthDel, message[2].lower())) self.clientChannelOps.remove(channelAuthDel) # }}} # {{{ dispatchPing(): Dispatch single PING message from server @@ -115,13 +115,13 @@ class IrcMiRCARTBot(IrcClient.IrcClient): if message[2].lower() == self.clientChannel.lower() \ and message[3].startswith("!pngbot "): if (int(time.time()) - self.clientLastMessage) < 45: - print("Ignoring request on {} from {} due to rate limit: {}".format(message[2].lower(), message[0], message[3])) + self._log("Ignoring request on {} from {} due to rate limit: {}".format(message[2].lower(), message[0], message[3])) return elif message[0].split("!")[0].lower() not in self.clientChannelOps: - print("Ignoring request on {} from {} due to lack of authorisation: {}".format(message[2].lower(), message[0], message[3])) + self._log("Ignoring request on {} from {} due to lack of authorisation: {}".format(message[2].lower(), message[0], message[3])) return else: - print("Processing request on {} from {}: {}".format(message[2].lower(), message[0], message[3])) + self._log("Processing request on {} from {}: {}".format(message[2].lower(), message[0], message[3])) self.clientLastMessage = int(time.time()) asciiUrl = message[3].split(" ")[1] asciiTmpFilePath = "tmp.txt"; imgTmpFilePath = "tmp.png"; @@ -133,10 +133,10 @@ class IrcMiRCARTBot(IrcClient.IrcClient): _MiRCART = mirc2png.MiRCART(asciiTmpFilePath, imgTmpFilePath, "DejaVuSansMono.ttf", 11) imgurResponse = self.uploadToImgur(imgTmpFilePath, "MiRCART image", "MiRCART image", "c9a6efb3d7932fd") if imgurResponse[0] == 200: - print("Uploaded as: {}".format(imgurResponse[1])) + self._log("Uploaded as: {}".format(imgurResponse[1])) self.sendline("PRIVMSG", message[2], "8/!\\ Uploaded as: {}".format(imgurResponse[1])) else: - print("Upload failed with HTTP status code {}".format(imgurResponse[0])) + self._log("Upload failed with HTTP status code {}".format(imgurResponse[0])) self.sendline("PRIVMSG", message[2], "4/!\\ Uploaded failed with HTTP status code {}!".format(imgurResponse[0])) if os.path.isfile(asciiTmpFilePath): os.remove(asciiTmpFilePath) @@ -146,7 +146,7 @@ class IrcMiRCARTBot(IrcClient.IrcClient): # {{{ dispatchTimer(): Dispatch single client timer expiration def dispatchTimer(self): if self.clientChannelRejoin: - print("Attempting to join {} on {}:{}...".format(self.clientChannel, self.serverHname, self.serverPort)) + self._log("Attempting to join {} on {}:{}...".format(self.clientChannel, self.serverHname, self.serverPort)) self.sendline("JOIN", self.clientChannel) self.clientNextTimeout = time.time() + 15; self.clientChannelRejoin = True; # }}} @@ -177,6 +177,10 @@ class IrcMiRCARTBot(IrcClient.IrcClient): elif serverMessage[1] == "PRIVMSG": self.dispatchPrivmsg(serverMessage) # }}} + # {{{ self._log(): Log single message to stdout w/ timestamp + def _log(self, msg): + print(time.strftime("%Y/%m/%d %H:%M:%S") + " " + msg) + # }}} # {{{ uploadToImgur(): Upload single file to Imgur def uploadToImgur(self, imgFilePath, imgName, imgTitle, apiKey): requestImageData = open(imgFilePath, "rb").read() From e182f0ffa9db1620c0f5be147d21858343e63071 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Wed, 3 Jan 2018 03:33:57 +0100 Subject: [PATCH 032/148] {IrcMiRCARTBot,MiRCART}.py: differentiate private class member functions. --- IrcClient.py | 12 ++--- IrcMiRCARTBot.py | 128 +++++++++++++++++++++++------------------------ MiRCART.py | 64 ++++++++++++------------ 3 files changed, 102 insertions(+), 102 deletions(-) diff --git a/IrcClient.py b/IrcClient.py index b8eccb9..423cb55 100644 --- a/IrcClient.py +++ b/IrcClient.py @@ -32,6 +32,12 @@ class IrcClient: clientSocket = clientSocketFile = None; clientNextTimeout = None + # {{{ close(): Close connection to server + def close(self): + if self.clientSocket != None: + self.clientSocket.close() + self.clientSocket = self.clientSocketFile = None; + # }}} # {{{ connect(): Connect to server and register w/ optional timeout def connect(self, timeout=None): self.clientSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -51,12 +57,6 @@ class IrcClient: self.sendline("USER", self.clientIdent, "0", "0", self.clientGecos) return True # }}} - # {{{ close(): Close connection to server - def close(self): - if self.clientSocket != None: - self.clientSocket.close() - self.clientSocket = self.clientSocketFile = None; - # }}} # {{{ readline(): Read and parse single line from server into canonicalised list, honouring timers def readline(self): if self.clientNextTimeout: diff --git a/IrcMiRCARTBot.py b/IrcMiRCARTBot.py index a2d1a50..bcb049f 100755 --- a/IrcMiRCARTBot.py +++ b/IrcMiRCARTBot.py @@ -33,31 +33,14 @@ class IrcMiRCARTBot(IrcClient.IrcClient): clientChannelLastMessage = clientChannelOps = clientChannel = None clientChannelRejoin = None - # {{{ connect(): Connect to server and (re)initialise w/ optional timeout - def connect(self, timeout=None): - self._log("Connecting to {}:{}...".format(self.serverHname, self.serverPort)) - if super().connect(timeout): - self._log("Connected to {}:{}.".format(self.serverHname, self.serverPort)) - self._log("Registering on {}:{} as {}, {}, {}...".format(self.serverHname, self.serverPort, self.clientNick, self.clientIdent, self.clientGecos)) - self.clientLastMessage = 0; self.clientChannelOps = []; - self.clientChannelRejoin = False - return True - else: - return False - # }}} - # {{{ dispatchNone(): Dispatch None message from server - def dispatchNone(self): - self._log("Disconnected from {}:{}.".format(self.serverHname, self.serverPort)) - self.close() - # }}} - # {{{ dispatch001(): Dispatch single 001 (RPL_WELCOME) - def dispatch001(self, message): + # {{{ _dispatch001(): Dispatch single 001 (RPL_WELCOME) + def _dispatch001(self, message): self._log("Registered on {}:{} as {}, {}, {}.".format(self.serverHname, self.serverPort, self.clientNick, self.clientIdent, self.clientGecos)) self._log("Attempting to join {} on {}:{}...".format(self.clientChannel, self.serverHname, self.serverPort)) self.sendline("JOIN", self.clientChannel) # }}} - # {{{ dispatch353(): Dispatch single 353 (RPL_NAMREPLY) - def dispatch353(self, message): + # {{{ _dispatch353(): Dispatch single 353 (RPL_NAMREPLY) + def _dispatch353(self, message): if message[4].lower() == self.clientChannel.lower(): for channelNickSpec in message[5].split(" "): if len(channelNickSpec) \ @@ -66,20 +49,20 @@ class IrcMiRCARTBot(IrcClient.IrcClient): self.clientChannelOps.append(channelNickSpec[1:].lower()) self._log("Authorising {} on {}".format(channelNickSpec[1:].lower(), message[4].lower())) # }}} - # {{{ dispatchJoin(): Dispatch single JOIN message from server - def dispatchJoin(self, message): + # {{{ _dispatchJoin(): Dispatch single JOIN message from server + def _dispatchJoin(self, message): self._log("Joined {} on {}:{}.".format(message[2].lower(), self.serverHname, self.serverPort)) self.clientNextTimeout = None; self.clientChannelRejoin = False; # }}} - # {{{ dispatchKick(): Dispatch single KICK message from server - def dispatchKick(self, message): + # {{{ _dispatchKick(): Dispatch single KICK message from server + def _dispatchKick(self, message): if message[2].lower() == self.clientChannel.lower() \ and message[3].lower() == self.clientNick.lower(): self._log("Kicked from {} by {}, rejoining in 15 seconds".format(message[2].lower(), message[0])) self.clientNextTimeout = time.time() + 15; self.clientChannelRejoin = True; # }}} - # {{{ dispatchMode(): Dispatch single MODE message from server - def dispatchMode(self, message): + # {{{ _dispatchMode(): Dispatch single MODE message from server + def _dispatchMode(self, message): if message[2].lower() == self.clientChannel.lower(): channelModeType = "+"; channelModeArg = 4; channelAuthAdd = ""; channelAuthDel = ""; @@ -106,12 +89,17 @@ class IrcMiRCARTBot(IrcClient.IrcClient): self._log("Deauthorising {} on {}".format(channelAuthDel, message[2].lower())) self.clientChannelOps.remove(channelAuthDel) # }}} - # {{{ dispatchPing(): Dispatch single PING message from server - def dispatchPing(self, message): + # {{{ _dispatchNone(): Dispatch None message from server + def _dispatchNone(self): + self._log("Disconnected from {}:{}.".format(self.serverHname, self.serverPort)) + self.close() + # }}} + # {{{ _dispatchPing(): Dispatch single PING message from server + def _dispatchPing(self, message): self.sendline("PONG", message[2]) # }}} - # {{{ dispatchPrivmsg(): Dispatch single PRIVMSG message from server - def dispatchPrivmsg(self, message): + # {{{ _dispatchPrivmsg(): Dispatch single PRIVMSG message from server + def _dispatchPrivmsg(self, message): if message[2].lower() == self.clientChannel.lower() \ and message[3].startswith("!pngbot "): if (int(time.time()) - self.clientLastMessage) < 45: @@ -131,7 +119,7 @@ class IrcMiRCARTBot(IrcClient.IrcClient): os.remove(imgTmpFilePath) urllib.request.urlretrieve(asciiUrl, asciiTmpFilePath) _MiRCART = mirc2png.MiRCART(asciiTmpFilePath, imgTmpFilePath, "DejaVuSansMono.ttf", 11) - imgurResponse = self.uploadToImgur(imgTmpFilePath, "MiRCART image", "MiRCART image", "c9a6efb3d7932fd") + imgurResponse = self._uploadToImgur(imgTmpFilePath, "MiRCART image", "MiRCART image", "c9a6efb3d7932fd") if imgurResponse[0] == 200: self._log("Uploaded as: {}".format(imgurResponse[1])) self.sendline("PRIVMSG", message[2], "8/!\\ Uploaded as: {}".format(imgurResponse[1])) @@ -143,46 +131,19 @@ class IrcMiRCARTBot(IrcClient.IrcClient): if os.path.isfile(imgTmpFilePath): os.remove(imgTmpFilePath) # }}} - # {{{ dispatchTimer(): Dispatch single client timer expiration - def dispatchTimer(self): + # {{{ _dispatchTimer(): Dispatch single client timer expiration + def _dispatchTimer(self): if self.clientChannelRejoin: self._log("Attempting to join {} on {}:{}...".format(self.clientChannel, self.serverHname, self.serverPort)) self.sendline("JOIN", self.clientChannel) self.clientNextTimeout = time.time() + 15; self.clientChannelRejoin = True; # }}} - # {{{ dispatch(): Read, parse, and dispatch single line from server - def dispatch(self): - while True: - if self.clientNextTimeout: - timeNow = time.time() - if self.clientNextTimeout <= timeNow: - self.dispatchTimer() - serverMessage = self.readline() - if serverMessage == None: - self.dispatchNone(); break; - elif serverMessage == "": - continue - elif serverMessage[1] == "001": - self.dispatch001(serverMessage) - elif serverMessage[1] == "353": - self.dispatch353(serverMessage) - elif serverMessage[1] == "JOIN": - self.dispatchJoin(serverMessage) - elif serverMessage[1] == "KICK": - self.dispatchKick(serverMessage) - elif serverMessage[1] == "MODE": - self.dispatchMode(serverMessage) - elif serverMessage[1] == "PING": - self.dispatchPing(serverMessage) - elif serverMessage[1] == "PRIVMSG": - self.dispatchPrivmsg(serverMessage) - # }}} - # {{{ self._log(): Log single message to stdout w/ timestamp + # {{{ _log(): Log single message to stdout w/ timestamp def _log(self, msg): print(time.strftime("%Y/%m/%d %H:%M:%S") + " " + msg) # }}} - # {{{ uploadToImgur(): Upload single file to Imgur - def uploadToImgur(self, imgFilePath, imgName, imgTitle, apiKey): + # {{{ _uploadToImgur(): Upload single file to Imgur + def _uploadToImgur(self, imgFilePath, imgName, imgTitle, apiKey): requestImageData = open(imgFilePath, "rb").read() requestData = { \ "image": base64.b64encode(requestImageData), \ @@ -199,6 +160,45 @@ class IrcMiRCARTBot(IrcClient.IrcClient): else: return [responseHttp.status_code] # }}} + # {{{ connect(): Connect to server and (re)initialise w/ optional timeout + def connect(self, timeout=None): + self._log("Connecting to {}:{}...".format(self.serverHname, self.serverPort)) + if super().connect(timeout): + self._log("Connected to {}:{}.".format(self.serverHname, self.serverPort)) + self._log("Registering on {}:{} as {}, {}, {}...".format(self.serverHname, self.serverPort, self.clientNick, self.clientIdent, self.clientGecos)) + self.clientLastMessage = 0; self.clientChannelOps = []; + self.clientChannelRejoin = False + return True + else: + return False + # }}} + # {{{ dispatch(): Read, parse, and dispatch single line from server + def dispatch(self): + while True: + if self.clientNextTimeout: + timeNow = time.time() + if self.clientNextTimeout <= timeNow: + self._dispatchTimer() + serverMessage = self.readline() + if serverMessage == None: + self._dispatchNone(); break; + elif serverMessage == "": + continue + elif serverMessage[1] == "001": + self._dispatch001(serverMessage) + elif serverMessage[1] == "353": + self._dispatch353(serverMessage) + elif serverMessage[1] == "JOIN": + self._dispatchJoin(serverMessage) + elif serverMessage[1] == "KICK": + self._dispatchKick(serverMessage) + elif serverMessage[1] == "MODE": + self._dispatchMode(serverMessage) + elif serverMessage[1] == "PING": + self._dispatchPing(serverMessage) + elif serverMessage[1] == "PRIVMSG": + self._dispatchPrivmsg(serverMessage) + # }}} # # Initialisation method diff --git a/MiRCART.py b/MiRCART.py index 9fac4cb..ead67fc 100755 --- a/MiRCART.py +++ b/MiRCART.py @@ -41,8 +41,8 @@ class MiRCART: state = None; inCurCol = None; - # {{{ ColourMapBold: mIRC colour number to RGBA map given ^B (bold) - ColourMapBold = [ + # {{{ _ColourMapBold: mIRC colour number to RGBA map given ^B (bold) + _ColourMapBold = [ (255, 255, 255, 255), # White (85, 85, 85, 255), # Grey (85, 85, 255, 255), # Light Blue @@ -61,8 +61,8 @@ class MiRCART: (255, 255, 255, 255), # White ] # }}} - # {{{ ColourMapNormal: mIRC colour number to RGBA map given none of ^[BFV_] (bold, italic, reverse, underline) - ColourMapNormal = [ + # {{{ _ColourMapNormal: mIRC colour number to RGBA map given none of ^[BFV_] (bold, italic, reverse, underline) + _ColourMapNormal = [ (255, 255, 255, 255), # White (0, 0, 0, 255), # Black (0, 0, 187, 255), # Blue @@ -81,41 +81,41 @@ class MiRCART: (187, 187, 187, 255), # Light Grey ] # }}} - # {{{ State: Parsing loop state - class State(Enum): + # {{{ _State: Parsing loop state + class _State(Enum): STATE_CHAR = 1 STATE_COLOUR_SPEC = 2 # }}} - # {{{ getMaxCols(): Calculate widest row in lines, ignoring non-printable & mIRC control code sequences - def getMaxCols(self, lines): + # {{{ _getMaxCols(): Calculate widest row in lines, ignoring non-printable & mIRC control code sequences + def _getMaxCols(self, lines): maxCols = 0; for curRow in range(0, len(lines)): - curRowCols = 0; curState = self.State.STATE_CHAR; + curRowCols = 0; curState = self._State.STATE_CHAR; curCol = 0; curColLen = len(lines[curRow]); while curCol < curColLen: curChar = lines[curRow][curCol] - if curState == self.State.STATE_CHAR: + if curState == self._State.STATE_CHAR: if curChar == "": - curState = self.State.STATE_COLOUR_SPEC; curCol += 1; + curState = self._State.STATE_COLOUR_SPEC; curCol += 1; elif curChar in string.printable: curRowCols += 1; curCol += 1; else: curCol += 1; - elif curState == self.State.STATE_COLOUR_SPEC: + elif curState == self._State.STATE_COLOUR_SPEC: if curChar in set(",0123456789"): curCol += 1; else: - curState = self.State.STATE_CHAR; + curState = self._State.STATE_CHAR; maxCols = max(maxCols, curRowCols) return maxCols # }}} - # {{{ parseAsChar(): Parse single character as regular character and mutate state - def parseAsChar(self, char): + # {{{ _parseAsChar(): Parse single character as regular character and mutate state + def _parseAsChar(self, char): if char == "": self.inCurCol += 1; self.inCurBold = 0 if self.inCurBold else 1; elif char == "": - self.state = self.State.STATE_COLOUR_SPEC; self.inCurCol += 1; + self._State = self._State.STATE_COLOUR_SPEC; self.inCurCol += 1; elif char == "": self.inCurCol += 1; self.inCurItalic = 0 if self.inCurItalic else 1; elif char == "": @@ -129,20 +129,20 @@ class MiRCART: self.inCurCol += 1; self.inCurUnderline = 0 if self.inCurUnderline else 1; elif char == " ": if self.inCurBold: - colourBg = self.ColourMapBold[self.outCurColourBg] + colourBg = self._ColourMapBold[self.outCurColourBg] else: - colourBg = self.ColourMapNormal[self.outCurColourBg] + colourBg = self._ColourMapNormal[self.outCurColourBg] self.outImgDraw.rectangle(((self.outCurX, self.outCurY), (self.outCurX + 7, self.outCurY + 14)), fill=colourBg) if self.inCurUnderline: self.outImgDraw.line((self.outCurX, self.outCurY + 11, self.outCurX + 7, self.outCurY + 11), fill=colourFg) self.outCurX += 7; self.inCurCol += 1; else: if self.inCurBold: - colourBg = self.ColourMapBold[self.outCurColourBg] - colourFg = self.ColourMapBold[self.outCurColourFg] + colourBg = self._ColourMapBold[self.outCurColourBg] + colourFg = self._ColourMapBold[self.outCurColourFg] else: - colourBg = self.ColourMapNormal[self.outCurColourBg] - colourFg = self.ColourMapNormal[self.outCurColourFg] + colourBg = self._ColourMapNormal[self.outCurColourBg] + colourFg = self._ColourMapNormal[self.outCurColourFg] self.outImgDraw.rectangle(((self.outCurX, self.outCurY), (self.outCurX + 7, self.outCurY + 14)), fill=colourBg) # XXX implement italic self.outImgDraw.text((self.outCurX, self.outCurY), char, colourFg, self.outImgFont) @@ -150,8 +150,8 @@ class MiRCART: self.outImgDraw.line((self.outCurX, self.outCurY + 11, self.outCurX + 7, self.outCurY + 11), fill=colourFg) self.outCurX += 7; self.inCurCol += 1; # }}} - # {{{ parseAsColourSpec(): Parse single character as mIRC colour control code sequence and mutate state - def parseAsColourSpec(self, char): + # {{{ _parseAsColourSpec(): Parse single character as mIRC colour control code sequence and mutate state + def _parseAsColourSpec(self, char): if char in set(",0123456789"): self.inCurColourSpec += char; self.inCurCol += 1; else: @@ -163,7 +163,7 @@ class MiRCART: self.outCurColourFg = int(self.inCurColourSpec[0]) else: self.outCurColourBg = 1; self.outCurColourFg = 15; - self.inCurColourSpec = ""; self.state = self.State.STATE_CHAR; + self.inCurColourSpec = ""; self._State = self._State.STATE_CHAR; # }}} # @@ -171,23 +171,23 @@ class MiRCART: def __init__(self, inFilePath, imgFilePath, fontFilePath="DejaVuSansMono.ttf", fontSize=11): self.inFilePath = inFilePath; self.inFile = open(inFilePath, "r"); self.inLines = self.inFile.readlines() - self.inColsMax = self.getMaxCols(self.inLines) + self.inColsMax = self._getMaxCols(self.inLines) self.inRows = len(self.inLines) self.outFontFilePath = fontFilePath; self.outFontSize = int(fontSize); - self.outImg = Image.new("RGBA", (self.inColsMax * 7, self.inRows * 14), self.ColourMapNormal[1]) + self.outImg = Image.new("RGBA", (self.inColsMax * 7, self.inRows * 14), self._ColourMapNormal[1]) self.outImgDraw = ImageDraw.Draw(self.outImg) self.outImgFont = ImageFont.truetype(self.outFontFilePath, self.outFontSize) self.outCurColourBg = 1; self.outCurColourFg = 15; self.outCurX = 0; self.outCurY = 0; for inCurRow in range(0, len(self.inLines)): self.inCurBold = 0; self.inCurItalic = 0; self.inCurUnderline = 0; - self.inCurColourSpec = ""; self.state = self.State.STATE_CHAR; + self.inCurColourSpec = ""; self._State = self._State.STATE_CHAR; self.inCurCol = 0; while self.inCurCol < len(self.inLines[inCurRow]): - if self.state == self.State.STATE_CHAR: - self.parseAsChar(self.inLines[inCurRow][self.inCurCol]) - elif self.state == self.State.STATE_COLOUR_SPEC: - self.parseAsColourSpec(self.inLines[inCurRow][self.inCurCol]) + if self._State == self._State.STATE_CHAR: + self._parseAsChar(self.inLines[inCurRow][self.inCurCol]) + elif self._State == self._State.STATE_COLOUR_SPEC: + self._parseAsColourSpec(self.inLines[inCurRow][self.inCurCol]) self.outCurX = 0; self.outCurY += 13; self.inFile.close(); self.outImg.save(imgFilePath); From 523d91ff894bfab7ee310f21dabd53523f887080 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Wed, 3 Jan 2018 03:52:41 +0100 Subject: [PATCH 033/148] MiRCART.py:MiRCART._parseAsChar(): derive rectangle & line coordinates and outCur{X,Y} offset from outImgFontSize[{0,1}]. MiRCART.py:MiRCART.__init__(): initialise outImgFontSize and add underline area offset. MiRCART.py:MiRCART.__init__(): derive outCurY offset from outImgFontSize[1]. --- MiRCART.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/MiRCART.py b/MiRCART.py index ead67fc..c15ff81 100755 --- a/MiRCART.py +++ b/MiRCART.py @@ -132,10 +132,10 @@ class MiRCART: colourBg = self._ColourMapBold[self.outCurColourBg] else: colourBg = self._ColourMapNormal[self.outCurColourBg] - self.outImgDraw.rectangle(((self.outCurX, self.outCurY), (self.outCurX + 7, self.outCurY + 14)), fill=colourBg) + self.outImgDraw.rectangle(((self.outCurX, self.outCurY), (self.outCurX + self.outImgFontSize[0], self.outCurY + self.outImgFontSize[1])), fill=colourBg) if self.inCurUnderline: - self.outImgDraw.line((self.outCurX, self.outCurY + 11, self.outCurX + 7, self.outCurY + 11), fill=colourFg) - self.outCurX += 7; self.inCurCol += 1; + self.outImgDraw.line((self.outCurX, self.outCurY + (self.outImgFontSize[1] - 2), self.outCurX + self.outImgFontSize[0], self.outCurY + (self.outImgFontSize[1] - 2)), fill=colourFg) + self.outCurX += self.outImgFontSize[0]; self.inCurCol += 1; else: if self.inCurBold: colourBg = self._ColourMapBold[self.outCurColourBg] @@ -143,12 +143,12 @@ class MiRCART: else: colourBg = self._ColourMapNormal[self.outCurColourBg] colourFg = self._ColourMapNormal[self.outCurColourFg] - self.outImgDraw.rectangle(((self.outCurX, self.outCurY), (self.outCurX + 7, self.outCurY + 14)), fill=colourBg) + self.outImgDraw.rectangle(((self.outCurX, self.outCurY), (self.outCurX + self.outImgFontSize[0], self.outCurY + self.outImgFontSize[1])), fill=colourBg) # XXX implement italic self.outImgDraw.text((self.outCurX, self.outCurY), char, colourFg, self.outImgFont) if self.inCurUnderline: - self.outImgDraw.line((self.outCurX, self.outCurY + 11, self.outCurX + 7, self.outCurY + 11), fill=colourFg) - self.outCurX += 7; self.inCurCol += 1; + self.outImgDraw.line((self.outCurX, self.outCurY + (self.outImgFontSize[1] - 2), self.outCurX + self.outImgFontSize[0], self.outCurY + (self.outImgFontSize[1] - 2)), fill=colourFg) + self.outCurX += self.outImgFontSize[0]; self.inCurCol += 1; # }}} # {{{ _parseAsColourSpec(): Parse single character as mIRC colour control code sequence and mutate state def _parseAsColourSpec(self, char): @@ -174,9 +174,10 @@ class MiRCART: self.inColsMax = self._getMaxCols(self.inLines) self.inRows = len(self.inLines) self.outFontFilePath = fontFilePath; self.outFontSize = int(fontSize); - self.outImg = Image.new("RGBA", (self.inColsMax * 7, self.inRows * 14), self._ColourMapNormal[1]) - self.outImgDraw = ImageDraw.Draw(self.outImg) self.outImgFont = ImageFont.truetype(self.outFontFilePath, self.outFontSize) + self.outImgFontSize = list(self.outImgFont.getsize(" ")); self.outImgFontSize[1] += 3; + self.outImg = Image.new("RGBA", (self.inColsMax * self.outImgFontSize[0], self.inRows * self.outImgFontSize[1]), self._ColourMapNormal[1]) + self.outImgDraw = ImageDraw.Draw(self.outImg) self.outCurColourBg = 1; self.outCurColourFg = 15; self.outCurX = 0; self.outCurY = 0; for inCurRow in range(0, len(self.inLines)): @@ -188,7 +189,7 @@ class MiRCART: self._parseAsChar(self.inLines[inCurRow][self.inCurCol]) elif self._State == self._State.STATE_COLOUR_SPEC: self._parseAsColourSpec(self.inLines[inCurRow][self.inCurCol]) - self.outCurX = 0; self.outCurY += 13; + self.outCurX = 0; self.outCurY += self.outImgFontSize[1]; self.inFile.close(); self.outImg.save(imgFilePath); From 6fcf0e0a4f8152b103efac6c594fc31bab8e01f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Wed, 3 Jan 2018 04:15:43 +0100 Subject: [PATCH 034/148] IrcClient.py:IrcClient.{queue,unqueue}(): split. IrcMiRCARTBot.py:IrcMiRCARTBot._dispatch{001,Ping,Privmsg,Timer}(): replace readline() calls w/ queue() calls. IrcMiRCARTBot.py:IrcMiRCARTBot.dispatch(): call unqueue() after processing timers and prior to calling readline(). --- IrcClient.py | 34 ++++++++++++++++++++++++---------- IrcMiRCARTBot.py | 11 ++++++----- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/IrcClient.py b/IrcClient.py index 423cb55..e475ba2 100644 --- a/IrcClient.py +++ b/IrcClient.py @@ -30,7 +30,7 @@ class IrcClient: serverHname = serverPort = None; clientNick = clientIdent = clientGecos = None; clientSocket = clientSocketFile = None; - clientNextTimeout = None + clientNextTimeout = None; clientQueue = None; # {{{ close(): Close connection to server def close(self): @@ -53,8 +53,9 @@ class IrcClient: else: select.select([], [self.clientSocket.fileno()], []) self.clientSocketFile = self.clientSocket.makefile() - self.sendline("NICK", self.clientNick) - self.sendline("USER", self.clientIdent, "0", "0", self.clientGecos) + self.clientQueue = [] + self.queue("NICK", self.clientNick) + self.queue("USER", self.clientIdent, "0", "0", self.clientGecos) return True # }}} # {{{ readline(): Read and parse single line from server into canonicalised list, honouring timers @@ -86,20 +87,33 @@ class IrcClient: msg = [""] + msg[0:] return msg # }}} - # {{{ sendline(): Parse and send single line to server from list, ignoring timers - def sendline(self, *args): + # {{{ queue(): Parse and queue single line to server from list + def queue(self, *args): msg = ""; argNumMax = len(args); for argNum in range(0, argNumMax): if argNum == (argNumMax - 1): msg += ":" + args[argNum] else: msg += args[argNum] + " " - msg = (msg + "\r\n").encode(); msgLen = len(msg); msgBytesSent = 0; - while msgBytesSent < msgLen: - readySet = select.select([], [self.clientSocket.fileno()], []) - msgBytesSent = self.clientSocket.send(msg) - msg = msg[msgBytesSent:]; msgLen -= msgBytesSent; + self.clientQueue.append(msg) # }}} + # {{{ unqueue(): Send all queued lines to server, honouring timers + def unqueue(self): + while self.clientQueue: + msg = self.clientQueue[0] + msg = (msg + "\r\n").encode(); msgLen = len(msg); msgBytesSent = 0; + while msgBytesSent < msgLen: + if self.clientNextTimeout: + timeNow = time.time() + if self.clientNextTimeout <= timeNow: + self.clientQueue[0] = msg; return; + else: + readySet = select.select([], [self.clientSocket.fileno()], [], self.clientNextTimeout - timeNow) + else: + readySet = select.select([], [self.clientSocket.fileno()], []) + msgBytesSent = self.clientSocket.send(msg) + msg = msg[msgBytesSent:]; msgLen -= msgBytesSent; + del self.clientQueue[0] # # Initialisation method diff --git a/IrcMiRCARTBot.py b/IrcMiRCARTBot.py index bcb049f..8c32884 100755 --- a/IrcMiRCARTBot.py +++ b/IrcMiRCARTBot.py @@ -37,7 +37,7 @@ class IrcMiRCARTBot(IrcClient.IrcClient): def _dispatch001(self, message): self._log("Registered on {}:{} as {}, {}, {}.".format(self.serverHname, self.serverPort, self.clientNick, self.clientIdent, self.clientGecos)) self._log("Attempting to join {} on {}:{}...".format(self.clientChannel, self.serverHname, self.serverPort)) - self.sendline("JOIN", self.clientChannel) + self.queue("JOIN", self.clientChannel) # }}} # {{{ _dispatch353(): Dispatch single 353 (RPL_NAMREPLY) def _dispatch353(self, message): @@ -96,7 +96,7 @@ class IrcMiRCARTBot(IrcClient.IrcClient): # }}} # {{{ _dispatchPing(): Dispatch single PING message from server def _dispatchPing(self, message): - self.sendline("PONG", message[2]) + self.queue("PONG", message[2]) # }}} # {{{ _dispatchPrivmsg(): Dispatch single PRIVMSG message from server def _dispatchPrivmsg(self, message): @@ -122,10 +122,10 @@ class IrcMiRCARTBot(IrcClient.IrcClient): imgurResponse = self._uploadToImgur(imgTmpFilePath, "MiRCART image", "MiRCART image", "c9a6efb3d7932fd") if imgurResponse[0] == 200: self._log("Uploaded as: {}".format(imgurResponse[1])) - self.sendline("PRIVMSG", message[2], "8/!\\ Uploaded as: {}".format(imgurResponse[1])) + self.queue("PRIVMSG", message[2], "8/!\\ Uploaded as: {}".format(imgurResponse[1])) else: self._log("Upload failed with HTTP status code {}".format(imgurResponse[0])) - self.sendline("PRIVMSG", message[2], "4/!\\ Uploaded failed with HTTP status code {}!".format(imgurResponse[0])) + self.queue("PRIVMSG", message[2], "4/!\\ Uploaded failed with HTTP status code {}!".format(imgurResponse[0])) if os.path.isfile(asciiTmpFilePath): os.remove(asciiTmpFilePath) if os.path.isfile(imgTmpFilePath): @@ -135,7 +135,7 @@ class IrcMiRCARTBot(IrcClient.IrcClient): def _dispatchTimer(self): if self.clientChannelRejoin: self._log("Attempting to join {} on {}:{}...".format(self.clientChannel, self.serverHname, self.serverPort)) - self.sendline("JOIN", self.clientChannel) + self.queue("JOIN", self.clientChannel) self.clientNextTimeout = time.time() + 15; self.clientChannelRejoin = True; # }}} # {{{ _log(): Log single message to stdout w/ timestamp @@ -179,6 +179,7 @@ class IrcMiRCARTBot(IrcClient.IrcClient): timeNow = time.time() if self.clientNextTimeout <= timeNow: self._dispatchTimer() + self.unqueue() serverMessage = self.readline() if serverMessage == None: self._dispatchNone(); break; From 95b706b4bd2339c52cc279a38cc1b09e302c98d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Wed, 3 Jan 2018 04:19:13 +0100 Subject: [PATCH 035/148] IrcMiRCARTBot.py:IrcMiRCARTBot._dispatchPrivmsg(): correctly instantiate MiRCART.MiRCART. --- IrcMiRCARTBot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IrcMiRCARTBot.py b/IrcMiRCARTBot.py index 8c32884..d00fc24 100755 --- a/IrcMiRCARTBot.py +++ b/IrcMiRCARTBot.py @@ -118,7 +118,7 @@ class IrcMiRCARTBot(IrcClient.IrcClient): if os.path.isfile(imgTmpFilePath): os.remove(imgTmpFilePath) urllib.request.urlretrieve(asciiUrl, asciiTmpFilePath) - _MiRCART = mirc2png.MiRCART(asciiTmpFilePath, imgTmpFilePath, "DejaVuSansMono.ttf", 11) + _MiRCART = MiRCART.MiRCART(asciiTmpFilePath, imgTmpFilePath, "DejaVuSansMono.ttf", 11) imgurResponse = self._uploadToImgur(imgTmpFilePath, "MiRCART image", "MiRCART image", "c9a6efb3d7932fd") if imgurResponse[0] == 200: self._log("Uploaded as: {}".format(imgurResponse[1])) From f2affda37e27007b921f5891737fbc8662552a22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Wed, 3 Jan 2018 04:24:18 +0100 Subject: [PATCH 036/148] IrcMiRCARTBot.py:IrcMiRCARTBot._dispatchPrivmsg(): catch urllib.error.HTTPError exception during download. IrcMiRCARTBot.py:IrcMiRCARTBot._dispatchPrivmsg(): fix typo. --- IrcMiRCARTBot.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/IrcMiRCARTBot.py b/IrcMiRCARTBot.py index d00fc24..3be31c9 100755 --- a/IrcMiRCARTBot.py +++ b/IrcMiRCARTBot.py @@ -117,7 +117,12 @@ class IrcMiRCARTBot(IrcClient.IrcClient): os.remove(asciiTmpFilePath) if os.path.isfile(imgTmpFilePath): os.remove(imgTmpFilePath) - urllib.request.urlretrieve(asciiUrl, asciiTmpFilePath) + try: + urllib.request.urlretrieve(asciiUrl, asciiTmpFilePath) + except urllib.error.HTTPError as err: + self._log("Download failed with HTTP status code {}".format(err.code)) + self.queue("PRIVMSG", message[2], "4/!\\ Download failed with HTTP status code {}!".format(err.code)) + return _MiRCART = MiRCART.MiRCART(asciiTmpFilePath, imgTmpFilePath, "DejaVuSansMono.ttf", 11) imgurResponse = self._uploadToImgur(imgTmpFilePath, "MiRCART image", "MiRCART image", "c9a6efb3d7932fd") if imgurResponse[0] == 200: @@ -125,7 +130,7 @@ class IrcMiRCARTBot(IrcClient.IrcClient): self.queue("PRIVMSG", message[2], "8/!\\ Uploaded as: {}".format(imgurResponse[1])) else: self._log("Upload failed with HTTP status code {}".format(imgurResponse[0])) - self.queue("PRIVMSG", message[2], "4/!\\ Uploaded failed with HTTP status code {}!".format(imgurResponse[0])) + self.queue("PRIVMSG", message[2], "4/!\\ Upload failed with HTTP status code {}!".format(imgurResponse[0])) if os.path.isfile(asciiTmpFilePath): os.remove(asciiTmpFilePath) if os.path.isfile(imgTmpFilePath): From 97d8caaf1ed8fb9884bccee52aa4cdf967317dd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Wed, 3 Jan 2018 15:12:28 +0100 Subject: [PATCH 037/148] IrcClient.py: add missing `# {{{'. --- IrcClient.py | 1 + 1 file changed, 1 insertion(+) diff --git a/IrcClient.py b/IrcClient.py index e475ba2..03e0e6f 100644 --- a/IrcClient.py +++ b/IrcClient.py @@ -114,6 +114,7 @@ class IrcClient: msgBytesSent = self.clientSocket.send(msg) msg = msg[msgBytesSent:]; msgLen -= msgBytesSent; del self.clientQueue[0] + # }}} # # Initialisation method From 9c34fe32209de6a8deca9cbfbd3036200841f12f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Wed, 3 Jan 2018 15:13:52 +0100 Subject: [PATCH 038/148] IrcClient.py:IrcClient.unqueue(): handle timeout from timed select(). --- IrcClient.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/IrcClient.py b/IrcClient.py index 03e0e6f..90b2629 100644 --- a/IrcClient.py +++ b/IrcClient.py @@ -109,6 +109,8 @@ class IrcClient: self.clientQueue[0] = msg; return; else: readySet = select.select([], [self.clientSocket.fileno()], [], self.clientNextTimeout - timeNow) + if len(readySet[1]) == 0: + self.clientQueue[0] = msg; return; else: readySet = select.select([], [self.clientSocket.fileno()], []) msgBytesSent = self.clientSocket.send(msg) From b71ca6af897af94adb03f961cb9257a51f213da8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Wed, 3 Jan 2018 15:18:28 +0100 Subject: [PATCH 039/148] IrcClient.py:IrcClient.{queue,unqueue}(): always CR NL-terminate messages & encode() before queuing. --- IrcClient.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/IrcClient.py b/IrcClient.py index 90b2629..161e1ca 100644 --- a/IrcClient.py +++ b/IrcClient.py @@ -95,13 +95,12 @@ class IrcClient: msg += ":" + args[argNum] else: msg += args[argNum] + " " - self.clientQueue.append(msg) + self.clientQueue.append((msg + "\r\n").encode()) # }}} # {{{ unqueue(): Send all queued lines to server, honouring timers def unqueue(self): while self.clientQueue: - msg = self.clientQueue[0] - msg = (msg + "\r\n").encode(); msgLen = len(msg); msgBytesSent = 0; + msg = self.clientQueue[0]; msgLen = len(msg); msgBytesSent = 0; while msgBytesSent < msgLen: if self.clientNextTimeout: timeNow = time.time() From 49705ad4bc4476b4013d39ff3683ca81d9d51fd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Wed, 3 Jan 2018 15:37:26 +0100 Subject: [PATCH 040/148] IrcMiRCARTBot.py:IrcMiRCARTBot.{ContentTooLargeException,_urlretrieveReportHook()}: restrict ASCII downloads to 1 MB in size. IrcMiRCARTBot.py:IrcMiRCARTBot._dispatchPrivmsg(): pass (static) _urlretrieveReportHook() to urllib.request.urlretrieve(). IrcMiRCARTBot.py:IrcMiRCARTBot._dispatchPrivmsg(): handle ContentTooLargeException, urllib.error.URLError, and ValueError exceptions. IrcMiRCARTBot.py:IrcMiRCARTBot._dispatchPrivmsg(): only reset clientLastMessage after successful completion. --- IrcMiRCARTBot.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/IrcMiRCARTBot.py b/IrcMiRCARTBot.py index 3be31c9..65674ae 100755 --- a/IrcMiRCARTBot.py +++ b/IrcMiRCARTBot.py @@ -33,6 +33,10 @@ class IrcMiRCARTBot(IrcClient.IrcClient): clientChannelLastMessage = clientChannelOps = clientChannel = None clientChannelRejoin = None + # {{{ ContentTooLargeException: Raised by _urlretrieveReportHook() given download size > 1 MB + class ContentTooLargeException(Exception): + pass + # }}} # {{{ _dispatch001(): Dispatch single 001 (RPL_WELCOME) def _dispatch001(self, message): self._log("Registered on {}:{} as {}, {}, {}.".format(self.serverHname, self.serverPort, self.clientNick, self.clientIdent, self.clientGecos)) @@ -110,7 +114,6 @@ class IrcMiRCARTBot(IrcClient.IrcClient): return else: self._log("Processing request on {} from {}: {}".format(message[2].lower(), message[0], message[3])) - self.clientLastMessage = int(time.time()) asciiUrl = message[3].split(" ")[1] asciiTmpFilePath = "tmp.txt"; imgTmpFilePath = "tmp.png"; if os.path.isfile(asciiTmpFilePath): @@ -118,16 +121,29 @@ class IrcMiRCARTBot(IrcClient.IrcClient): if os.path.isfile(imgTmpFilePath): os.remove(imgTmpFilePath) try: - urllib.request.urlretrieve(asciiUrl, asciiTmpFilePath) + urllib.request.urlretrieve(asciiUrl, asciiTmpFilePath, IrcMiRCARTBot._urlretrieveReportHook) + except IrcMiRCARTBot.ContentTooLargeException: + self._log("Download size exceeds quota of 1 MB!") + self.queue("PRIVMSG", message[2], "4/!\\ Download size exceeds quota of 1 MB!") + return except urllib.error.HTTPError as err: self._log("Download failed with HTTP status code {}".format(err.code)) self.queue("PRIVMSG", message[2], "4/!\\ Download failed with HTTP status code {}!".format(err.code)) return + except urllib.error.URLError as err: + self._log("Invalid URL specified!") + self.queue("PRIVMSG", message[2], "4/!\\ Invalid URL specified!") + return + except ValueError as err: + self._log("Unknown URL type specified!") + self.queue("PRIVMSG", message[2], "4/!\\ Unknown URL type specified!") + return _MiRCART = MiRCART.MiRCART(asciiTmpFilePath, imgTmpFilePath, "DejaVuSansMono.ttf", 11) imgurResponse = self._uploadToImgur(imgTmpFilePath, "MiRCART image", "MiRCART image", "c9a6efb3d7932fd") if imgurResponse[0] == 200: self._log("Uploaded as: {}".format(imgurResponse[1])) self.queue("PRIVMSG", message[2], "8/!\\ Uploaded as: {}".format(imgurResponse[1])) + self.clientLastMessage = int(time.time()) else: self._log("Upload failed with HTTP status code {}".format(imgurResponse[0])) self.queue("PRIVMSG", message[2], "4/!\\ Upload failed with HTTP status code {}!".format(imgurResponse[0])) @@ -165,6 +181,11 @@ class IrcMiRCARTBot(IrcClient.IrcClient): else: return [responseHttp.status_code] # }}} + # {{{ _urlretrieveReportHook(): Limit downloads to 1 MB + def _urlretrieveReportHook(count, blockSize, totalSize): + if (totalSize > pow(2,10)): + raise IrcMiRCARTBot.ContentTooLargeException + # }}} # {{{ connect(): Connect to server and (re)initialise w/ optional timeout def connect(self, timeout=None): self._log("Connecting to {}:{}...".format(self.serverHname, self.serverPort)) From 1a2dd5f6921cd74695b726042d355fc75aa99c8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Thu, 4 Jan 2018 16:24:06 +0100 Subject: [PATCH 041/148] MiRC2png.py: renamed from MiRCART.py. {IrcClient,IrcMiRCARTBot,MiRC2png}.py: update Vim modeline w/ `sw=4 ts=4'. {IrcMiRCARTBot,MiRC2png}.py: update header legend. IrcMiRCARTBot.py:IrcMiRCARTBot._dispatchPrivmsg(): lower rate limit to (once per) 30 seconds. IrcMiRCARTBot.py:IrcMiRCARTBot._dispatchPrivmsg(): eliminate useless instance variable. IrcMiRCARTBot.py:IrcMiRCARTBot._urlretrieveReportHook(): compare against correct limit of 1 MB (2**20.) MiRCART.py: initial commit. README.md: updated. --- IrcClient.py | 2 +- IrcMiRCARTBot.py | 12 +- MiRC2png.py | 211 +++++++++++++++++++++++++++++++++++ MiRCART.py | 285 +++++++++++++++++++++-------------------------- README.md | 12 +- 5 files changed, 356 insertions(+), 166 deletions(-) create mode 100755 MiRC2png.py diff --git a/IrcClient.py b/IrcClient.py index 161e1ca..61e6d31 100644 --- a/IrcClient.py +++ b/IrcClient.py @@ -123,4 +123,4 @@ class IrcClient: self.serverHname = serverHname; self.serverPort = serverPort; self.clientNick = clientNick; self.clientIdent = clientIdent; self.clientGecos = clientGecos; -# vim:expandtab foldmethod=marker sw=8 ts=8 tw=120 +# vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/IrcMiRCARTBot.py b/IrcMiRCARTBot.py index 65674ae..3af1764 100755 --- a/IrcMiRCARTBot.py +++ b/IrcMiRCARTBot.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# mirc2png -- convert ASCII w/ mIRC control codes to monospaced PNG (for EFnet #MiRCART) +# IrcMiRCARTBot.py -- XXX # Copyright (c) 2018 Lucio Andrés Illanes Albornoz # # Permission is hereby granted, free of charge, to any person obtaining a copy @@ -25,7 +25,7 @@ import base64 import os, sys, time import json -import IrcClient, MiRCART +import IrcClient, MiRC2png import requests, urllib.request class IrcMiRCARTBot(IrcClient.IrcClient): @@ -106,7 +106,7 @@ class IrcMiRCARTBot(IrcClient.IrcClient): def _dispatchPrivmsg(self, message): if message[2].lower() == self.clientChannel.lower() \ and message[3].startswith("!pngbot "): - if (int(time.time()) - self.clientLastMessage) < 45: + if (int(time.time()) - self.clientLastMessage) < 30: self._log("Ignoring request on {} from {} due to rate limit: {}".format(message[2].lower(), message[0], message[3])) return elif message[0].split("!")[0].lower() not in self.clientChannelOps: @@ -138,7 +138,7 @@ class IrcMiRCARTBot(IrcClient.IrcClient): self._log("Unknown URL type specified!") self.queue("PRIVMSG", message[2], "4/!\\ Unknown URL type specified!") return - _MiRCART = MiRCART.MiRCART(asciiTmpFilePath, imgTmpFilePath, "DejaVuSansMono.ttf", 11) + MiRC2png.MiRC2png(asciiTmpFilePath, imgTmpFilePath, "DejaVuSansMono.ttf", 11) imgurResponse = self._uploadToImgur(imgTmpFilePath, "MiRCART image", "MiRCART image", "c9a6efb3d7932fd") if imgurResponse[0] == 200: self._log("Uploaded as: {}".format(imgurResponse[1])) @@ -183,7 +183,7 @@ class IrcMiRCARTBot(IrcClient.IrcClient): # }}} # {{{ _urlretrieveReportHook(): Limit downloads to 1 MB def _urlretrieveReportHook(count, blockSize, totalSize): - if (totalSize > pow(2,10)): + if (totalSize > pow(2,20)): raise IrcMiRCARTBot.ContentTooLargeException # }}} # {{{ connect(): Connect to server and (re)initialise w/ optional timeout @@ -256,4 +256,4 @@ if __name__ == "__main__": else: main(*sys.argv) -# vim:expandtab foldmethod=marker sw=8 ts=8 tw=120 +# vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/MiRC2png.py b/MiRC2png.py new file mode 100755 index 0000000..adf3b44 --- /dev/null +++ b/MiRC2png.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python3 +# +# MiRC2png.py -- convert ASCII w/ mIRC control codes to monospaced PNG (for EFnet #MiRCART) +# Copyright (c) 2018 Lucio Andrés Illanes Albornoz +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +from enum import Enum +from PIL import Image, ImageDraw, ImageFont +import string, sys + +class MiRC2png: + """Abstraction over ASCIIs containing mIRC control codes""" + inFilePath = inFile = None; + inLines = inColsMax = inRows = None; + + outFontFilePath = outFontSize = None; + outImg = outImgDraw = outImgFont = None; + outCurColourBg = outCurColourFg = None; + outCurX = outCurY = None; + + inCurBold = inCurItalic = inCurUnderline = None; + inCurColourSpec = None; + state = None; + inCurCol = None; + + # {{{ _ColourMapBold: mIRC colour number to RGBA map given ^B (bold) + _ColourMapBold = [ + (255, 255, 255, 255), # White + (85, 85, 85, 255), # Grey + (85, 85, 255, 255), # Light Blue + (85, 255, 85, 255), # Light Green + (255, 85, 85, 255), # Light Red + (255, 85, 85, 255), # Light Red + (255, 85, 255, 255), # Pink + (255, 255, 85, 255), # Light Yellow + (255, 255, 85, 255), # Light Yellow + (85, 255, 85, 255), # Light Green + (85, 255, 255, 255), # Light Cyan + (85, 255, 255, 255), # Light Cyan + (85, 85, 255, 255), # Light Blue + (255, 85, 255, 255), # Pink + (85, 85, 85, 255), # Grey + (255, 255, 255, 255), # White + ] + # }}} + # {{{ _ColourMapNormal: mIRC colour number to RGBA map given none of ^[BFV_] (bold, italic, reverse, underline) + _ColourMapNormal = [ + (255, 255, 255, 255), # White + (0, 0, 0, 255), # Black + (0, 0, 187, 255), # Blue + (0, 187, 0, 255), # Green + (255, 85, 85, 255), # Light Red + (187, 0, 0, 255), # Red + (187, 0, 187, 255), # Purple + (187, 187, 0, 255), # Yellow + (255, 255, 85, 255), # Light Yellow + (85, 255, 85, 255), # Light Green + (0, 187, 187, 255), # Cyan + (85, 255, 255, 255), # Light Cyan + (85, 85, 255, 255), # Light Blue + (255, 85, 255, 255), # Pink + (85, 85, 85, 255), # Grey + (187, 187, 187, 255), # Light Grey + ] + # }}} + # {{{ _State: Parsing loop state + class _State(Enum): + STATE_CHAR = 1 + STATE_COLOUR_SPEC = 2 + # }}} + + # {{{ _getMaxCols(): Calculate widest row in lines, ignoring non-printable & mIRC control code sequences + def _getMaxCols(self, lines): + maxCols = 0; + for curRow in range(0, len(lines)): + curRowCols = 0; curState = self._State.STATE_CHAR; + curCol = 0; curColLen = len(lines[curRow]); + while curCol < curColLen: + curChar = lines[curRow][curCol] + if curState == self._State.STATE_CHAR: + if curChar == "": + curState = self._State.STATE_COLOUR_SPEC; curCol += 1; + elif curChar in string.printable: + curRowCols += 1; curCol += 1; + else: + curCol += 1; + elif curState == self._State.STATE_COLOUR_SPEC: + if curChar in set(",0123456789"): + curCol += 1; + else: + curState = self._State.STATE_CHAR; + maxCols = max(maxCols, curRowCols) + return maxCols + # }}} + # {{{ _parseAsChar(): Parse single character as regular character and mutate state + def _parseAsChar(self, char): + if char == "": + self.inCurCol += 1; self.inCurBold = 0 if self.inCurBold else 1; + elif char == "": + self._State = self._State.STATE_COLOUR_SPEC; self.inCurCol += 1; + elif char == "": + self.inCurCol += 1; self.inCurItalic = 0 if self.inCurItalic else 1; + elif char == "": + self.inCurCol += 1; + self.inCurBold = 0; self.inCurItalic = 0; self.inCurUnderline = 0; + self.inCurColourSpec = ""; + elif char == "": + self.inCurCol += 1 + self.outCurColourBg, self.outCurColourFg = self.outCurColourFg, self.outCurColourBg; + elif char == "": + self.inCurCol += 1; self.inCurUnderline = 0 if self.inCurUnderline else 1; + elif char == " ": + if self.inCurBold: + colourBg = self._ColourMapBold[self.outCurColourBg] + else: + colourBg = self._ColourMapNormal[self.outCurColourBg] + self.outImgDraw.rectangle(((self.outCurX, self.outCurY), (self.outCurX + self.outImgFontSize[0], self.outCurY + self.outImgFontSize[1])), fill=colourBg) + if self.inCurUnderline: + self.outImgDraw.line((self.outCurX, self.outCurY + (self.outImgFontSize[1] - 2), self.outCurX + self.outImgFontSize[0], self.outCurY + (self.outImgFontSize[1] - 2)), fill=colourFg) + self.outCurX += self.outImgFontSize[0]; self.inCurCol += 1; + else: + if self.inCurBold: + colourBg = self._ColourMapBold[self.outCurColourBg] + colourFg = self._ColourMapBold[self.outCurColourFg] + else: + colourBg = self._ColourMapNormal[self.outCurColourBg] + colourFg = self._ColourMapNormal[self.outCurColourFg] + self.outImgDraw.rectangle(((self.outCurX, self.outCurY), (self.outCurX + self.outImgFontSize[0], self.outCurY + self.outImgFontSize[1])), fill=colourBg) + # XXX implement italic + self.outImgDraw.text((self.outCurX, self.outCurY), char, colourFg, self.outImgFont) + if self.inCurUnderline: + self.outImgDraw.line((self.outCurX, self.outCurY + (self.outImgFontSize[1] - 2), self.outCurX + self.outImgFontSize[0], self.outCurY + (self.outImgFontSize[1] - 2)), fill=colourFg) + self.outCurX += self.outImgFontSize[0]; self.inCurCol += 1; + # }}} + # {{{ _parseAsColourSpec(): Parse single character as mIRC colour control code sequence and mutate state + def _parseAsColourSpec(self, char): + if char in set(",0123456789"): + self.inCurColourSpec += char; self.inCurCol += 1; + else: + self.inCurColourSpec = self.inCurColourSpec.split(",") + if len(self.inCurColourSpec) == 2: + self.outCurColourFg = int(self.inCurColourSpec[0]) + self.outCurColourBg = int(self.inCurColourSpec[1] or self.outCurColourBg) + elif len(self.inCurColourSpec) == 1: + self.outCurColourFg = int(self.inCurColourSpec[0]) + else: + self.outCurColourBg = 1; self.outCurColourFg = 15; + self.inCurColourSpec = ""; self._State = self._State.STATE_CHAR; + # }}} + + # + # Initialisation method + def __init__(self, inFilePath, imgFilePath, fontFilePath="DejaVuSansMono.ttf", fontSize=11): + self.inFilePath = inFilePath; self.inFile = open(inFilePath, "r"); + self.inLines = self.inFile.readlines() + self.inColsMax = self._getMaxCols(self.inLines) + self.inRows = len(self.inLines) + self.outFontFilePath = fontFilePath; self.outFontSize = int(fontSize); + self.outImgFont = ImageFont.truetype(self.outFontFilePath, self.outFontSize) + self.outImgFontSize = list(self.outImgFont.getsize(" ")); self.outImgFontSize[1] += 3; + self.outImg = Image.new("RGBA", (self.inColsMax * self.outImgFontSize[0], self.inRows * self.outImgFontSize[1]), self._ColourMapNormal[1]) + self.outImgDraw = ImageDraw.Draw(self.outImg) + self.outCurColourBg = 1; self.outCurColourFg = 15; + self.outCurX = 0; self.outCurY = 0; + for inCurRow in range(0, len(self.inLines)): + self.inCurBold = 0; self.inCurItalic = 0; self.inCurUnderline = 0; + self.inCurColourSpec = ""; self._State = self._State.STATE_CHAR; + self.inCurCol = 0; + while self.inCurCol < len(self.inLines[inCurRow]): + if self._State == self._State.STATE_CHAR: + self._parseAsChar(self.inLines[inCurRow][self.inCurCol]) + elif self._State == self._State.STATE_COLOUR_SPEC: + self._parseAsColourSpec(self.inLines[inCurRow][self.inCurCol]) + self.outCurX = 0; self.outCurY += self.outImgFontSize[1]; + self.inFile.close(); + self.outImg.save(imgFilePath); + +# +# Entry point +def main(*argv): + MiRC2png(*argv[1:]) +if __name__ == "__main__": + if ((len(sys.argv) - 1) < 2)\ + or ((len(sys.argv) - 1) > 4): + print("usage: {} " \ + " " \ + " " \ + "[] " \ + "[]".format(sys.argv[0]), file=sys.stderr) + else: + main(*sys.argv) + +# vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/MiRCART.py b/MiRCART.py index c15ff81..3cf69b8 100755 --- a/MiRCART.py +++ b/MiRCART.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# mirc2png -- convert ASCII w/ mIRC control codes to monospaced PNG (for EFnet #MiRCART) +# MiRCART.py -- XXX # Copyright (c) 2018 Lucio Andrés Illanes Albornoz # # Permission is hereby granted, free of charge, to any person obtaining a copy @@ -22,47 +22,18 @@ # SOFTWARE. # -from enum import Enum -from PIL import Image, ImageDraw, ImageFont -import string, sys +import wx +import sys -class MiRCART: - """Abstraction over ASCIIs containing mIRC control codes""" - inFilePath = inFile = None; - inLines = inColsMax = inRows = None; - - outFontFilePath = outFontSize = None; - outImg = outImgDraw = outImgFont = None; - outCurColourBg = outCurColourFg = None; - outCurX = outCurY = None; - - inCurBold = inCurItalic = inCurUnderline = None; - inCurColourSpec = None; - state = None; - inCurCol = None; - - # {{{ _ColourMapBold: mIRC colour number to RGBA map given ^B (bold) - _ColourMapBold = [ - (255, 255, 255, 255), # White - (85, 85, 85, 255), # Grey - (85, 85, 255, 255), # Light Blue - (85, 255, 85, 255), # Light Green - (255, 85, 85, 255), # Light Red - (255, 85, 85, 255), # Light Red - (255, 85, 255, 255), # Pink - (255, 255, 85, 255), # Light Yellow - (255, 255, 85, 255), # Light Yellow - (85, 255, 85, 255), # Light Green - (85, 255, 255, 255), # Light Cyan - (85, 255, 255, 255), # Light Cyan - (85, 85, 255, 255), # Light Blue - (255, 85, 255, 255), # Pink - (85, 85, 85, 255), # Grey - (255, 255, 255, 255), # White - ] - # }}} - # {{{ _ColourMapNormal: mIRC colour number to RGBA map given none of ^[BFV_] (bold, italic, reverse, underline) - _ColourMapNormal = [ +class MiRCARTCanvas(wx.Panel): + """XXX""" + canvasPos = canvasSize = None + canvasMap = None + cellPos = cellSize = None + brushBg = brushFg = penBg = penFg = None + mircBg = mircFg = None + # {{{ mircColours: mIRC colour number to RGBA map given none of ^[BFV_] (bold, italic, reverse, underline) + mircColours = [ (255, 255, 255, 255), # White (0, 0, 0, 255), # Black (0, 0, 187, 255), # Blue @@ -81,131 +52,133 @@ class MiRCART: (187, 187, 187, 255), # Light Grey ] # }}} - # {{{ _State: Parsing loop state - class _State(Enum): - STATE_CHAR = 1 - STATE_COLOUR_SPEC = 2 - # }}} - # {{{ _getMaxCols(): Calculate widest row in lines, ignoring non-printable & mIRC control code sequences - def _getMaxCols(self, lines): - maxCols = 0; - for curRow in range(0, len(lines)): - curRowCols = 0; curState = self._State.STATE_CHAR; - curCol = 0; curColLen = len(lines[curRow]); - while curCol < curColLen: - curChar = lines[curRow][curCol] - if curState == self._State.STATE_CHAR: - if curChar == "": - curState = self._State.STATE_COLOUR_SPEC; curCol += 1; - elif curChar in string.printable: - curRowCols += 1; curCol += 1; - else: - curCol += 1; - elif curState == self._State.STATE_COLOUR_SPEC: - if curChar in set(",0123456789"): - curCol += 1; - else: - curState = self._State.STATE_CHAR; - maxCols = max(maxCols, curRowCols) - return maxCols + # {{{ _onMouseEvent(): XXX + def _onMouseEvent(self, event): + eventObject = event.GetEventObject() + if event.Dragging(): + eventDc = wx.ClientDC(self) + eventPoint = event.GetLogicalPosition(eventDc) + rectX = eventPoint.x - (eventPoint.x % self.cellSize[0]) + rectY = eventPoint.y - (eventPoint.y % self.cellSize[1]) + mapX = int(rectX / 7 if rectX else 0) + mapY = int(rectY / 14 if rectY else 0) + eventDc.SetBackground(self.brushBg); + if event.LeftIsDown(): + eventDc.SetBrush(self.brushFg); + eventDc.SetPen(self.penFg) + self.canvasMap[mapX][mapY] = [self.mircFg, self.mircFg, " "] + elif event.RightIsDown(): + eventDc.SetBrush(self.brushBg); + eventDc.SetPen(self.penBg) + self.canvasMap[mapX][mapY] = [self.mircBg, self.mircBg, " "] + eventDc.DrawRectangle(rectX, rectY, \ + self.cellSize[0], self.cellSize[1]) # }}} - # {{{ _parseAsChar(): Parse single character as regular character and mutate state - def _parseAsChar(self, char): - if char == "": - self.inCurCol += 1; self.inCurBold = 0 if self.inCurBold else 1; - elif char == "": - self._State = self._State.STATE_COLOUR_SPEC; self.inCurCol += 1; - elif char == "": - self.inCurCol += 1; self.inCurItalic = 0 if self.inCurItalic else 1; - elif char == "": - self.inCurCol += 1; - self.inCurBold = 0; self.inCurItalic = 0; self.inCurUnderline = 0; - self.inCurColourSpec = ""; - elif char == "": - self.inCurCol += 1 - self.outCurColourBg, self.outCurColourFg = self.outCurColourFg, self.outCurColourBg; - elif char == "": - self.inCurCol += 1; self.inCurUnderline = 0 if self.inCurUnderline else 1; - elif char == " ": - if self.inCurBold: - colourBg = self._ColourMapBold[self.outCurColourBg] - else: - colourBg = self._ColourMapNormal[self.outCurColourBg] - self.outImgDraw.rectangle(((self.outCurX, self.outCurY), (self.outCurX + self.outImgFontSize[0], self.outCurY + self.outImgFontSize[1])), fill=colourBg) - if self.inCurUnderline: - self.outImgDraw.line((self.outCurX, self.outCurY + (self.outImgFontSize[1] - 2), self.outCurX + self.outImgFontSize[0], self.outCurY + (self.outImgFontSize[1] - 2)), fill=colourFg) - self.outCurX += self.outImgFontSize[0]; self.inCurCol += 1; - else: - if self.inCurBold: - colourBg = self._ColourMapBold[self.outCurColourBg] - colourFg = self._ColourMapBold[self.outCurColourFg] - else: - colourBg = self._ColourMapNormal[self.outCurColourBg] - colourFg = self._ColourMapNormal[self.outCurColourFg] - self.outImgDraw.rectangle(((self.outCurX, self.outCurY), (self.outCurX + self.outImgFontSize[0], self.outCurY + self.outImgFontSize[1])), fill=colourBg) - # XXX implement italic - self.outImgDraw.text((self.outCurX, self.outCurY), char, colourFg, self.outImgFont) - if self.inCurUnderline: - self.outImgDraw.line((self.outCurX, self.outCurY + (self.outImgFontSize[1] - 2), self.outCurX + self.outImgFontSize[0], self.outCurY + (self.outImgFontSize[1] - 2)), fill=colourFg) - self.outCurX += self.outImgFontSize[0]; self.inCurCol += 1; + # {{{ onCharHook(): XXX + def onCharHook(self, event): + keyCode = event.GetKeyCode() + if keyCode == wx.WXK_UP: + self.mircFg = self.mircFg + 1 if self.mircFg < 15 else 15 + elif keyCode == wx.WXK_DOWN: + self.mircFg = self.mircFg - 1 if self.mircFg > 0 else 0 + self.brushBg = wx.Brush(wx.Colour(self.mircColours[self.mircBg]), wx.BRUSHSTYLE_SOLID) + self.brushFg = wx.Brush(wx.Colour(self.mircColours[self.mircFg]), wx.BRUSHSTYLE_SOLID) + self.penBg = wx.Pen(wx.Colour(self.mircColours[self.mircBg]), 1) + self.penFg = wx.Pen(wx.Colour(self.mircColours[self.mircFg]), 1) # }}} - # {{{ _parseAsColourSpec(): Parse single character as mIRC colour control code sequence and mutate state - def _parseAsColourSpec(self, char): - if char in set(",0123456789"): - self.inCurColourSpec += char; self.inCurCol += 1; - else: - self.inCurColourSpec = self.inCurColourSpec.split(",") - if len(self.inCurColourSpec) == 2: - self.outCurColourFg = int(self.inCurColourSpec[0]) - self.outCurColourBg = int(self.inCurColourSpec[1] or self.outCurColourBg) - elif len(self.inCurColourSpec) == 1: - self.outCurColourFg = int(self.inCurColourSpec[0]) - else: - self.outCurColourBg = 1; self.outCurColourFg = 15; - self.inCurColourSpec = ""; self._State = self._State.STATE_CHAR; + # {{{ onLeftDown(): XXX + def onLeftDown(self, event): + self._onMouseEvent(event) + # }}} + # {{{ onMotion(): XXX + def onMotion(self, event): + self._onMouseEvent(event) + # }}} + # {{{ onPaint(): XXX + def onPaint(self, event): + eventDc = wx.BufferedPaintDC(self) + eventDc.SetBackground(wx.Brush(wx.BLACK)) + eventDc.Clear() + for cellX in range(0, self.canvasSize[0]): + for cellY in range(0, self.canvasSize[1]): + eventDc.SetBackground(wx.Brush(wx.Colour(self.mircColours[self.canvasMap[cellX][cellY][0]]), wx.BRUSHSTYLE_SOLID)) + eventDc.SetBrush(wx.Brush(wx.Colour(self.mircColours[self.canvasMap[cellX][cellY][1]]), wx.BRUSHSTYLE_SOLID)) + eventDc.SetPen(wx.Pen(wx.Colour(self.mircColours[self.canvasMap[cellX][cellY][1]]), 1)) + rectX = cellX * self.cellSize[0]; rectY = cellY * self.cellSize[1]; + eventDc.DrawRectangle(rectX, rectY, \ + self.cellSize[0], self.cellSize[1]) + # }}} + # {{{ onRightDown(): XXX + def onRightDown(self, event): + self._onMouseEvent(event) # }}} # # Initialisation method - def __init__(self, inFilePath, imgFilePath, fontFilePath="DejaVuSansMono.ttf", fontSize=11): - self.inFilePath = inFilePath; self.inFile = open(inFilePath, "r"); - self.inLines = self.inFile.readlines() - self.inColsMax = self._getMaxCols(self.inLines) - self.inRows = len(self.inLines) - self.outFontFilePath = fontFilePath; self.outFontSize = int(fontSize); - self.outImgFont = ImageFont.truetype(self.outFontFilePath, self.outFontSize) - self.outImgFontSize = list(self.outImgFont.getsize(" ")); self.outImgFontSize[1] += 3; - self.outImg = Image.new("RGBA", (self.inColsMax * self.outImgFontSize[0], self.inRows * self.outImgFontSize[1]), self._ColourMapNormal[1]) - self.outImgDraw = ImageDraw.Draw(self.outImg) - self.outCurColourBg = 1; self.outCurColourFg = 15; - self.outCurX = 0; self.outCurY = 0; - for inCurRow in range(0, len(self.inLines)): - self.inCurBold = 0; self.inCurItalic = 0; self.inCurUnderline = 0; - self.inCurColourSpec = ""; self._State = self._State.STATE_CHAR; - self.inCurCol = 0; - while self.inCurCol < len(self.inLines[inCurRow]): - if self._State == self._State.STATE_CHAR: - self._parseAsChar(self.inLines[inCurRow][self.inCurCol]) - elif self._State == self._State.STATE_COLOUR_SPEC: - self._parseAsColourSpec(self.inLines[inCurRow][self.inCurCol]) - self.outCurX = 0; self.outCurY += self.outImgFontSize[1]; - self.inFile.close(); - self.outImg.save(imgFilePath); + def __init__(self, parent, canvasPos, cellSize, canvasSize): + super().__init__(parent, pos=canvasPos, size=( \ + cellSize[0] * canvasSize[0], + cellSize[1] * canvasSize[1])) + + self.canvasPos = canvasPos; self.canvasSize = canvasSize; + self.canvasMap = [[[1, 1, " "] for y in range(canvasSize[1])] for x in range(canvasSize[0])] + self.cellPos = (0, 0); self.cellSize = cellSize; + self.brushBg = wx.Brush(wx.Colour(self.mircColours[1]), wx.BRUSHSTYLE_SOLID) + self.brushFg = wx.Brush(wx.Colour(self.mircColours[4]), wx.BRUSHSTYLE_SOLID) + self.penBg = wx.Pen(wx.Colour(self.mircColours[1]), 1) + self.penFg = wx.Pen(wx.Colour(self.mircColours[4]), 1) + self.mircBg = 1; self.mircFg = 4; + self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM) + + self.Bind(wx.EVT_CHAR_HOOK, self.onCharHook) + self.Bind(wx.EVT_LEFT_DOWN, self.onLeftDown) + self.Bind(wx.EVT_MOTION, self.onMotion) + self.Bind(wx.EVT_PAINT, self.onPaint) + self.Bind(wx.EVT_RIGHT_DOWN, self.onRightDown) + +class MiRCARTFrame(wx.Frame): + """XXX""" + menuFile = menuFileSaveAs = menuFileExit = menuBar = None + panelSkin = panelCanvas = None + + # {{{ onFileSaveAs(): XXX + def onFileSaveAs(self, event): + pass + # }}} + # {{{ onFileExit(): XXX + def onFileExit(self, event): + self.Close(True) + # }}} + + # + # Initialisation method + def __init__(self, parent, appSize=(1024, 768), canvasPos=(25, 25), cellSize=(7, 14), canvasSize=(80, 25)): + super().__init__(parent, wx.ID_ANY, "MiRCART", size=appSize) + + self.menuFile = wx.Menu() + self.menuFileExit = self.menuFile.Append(wx.ID_EXIT, "E&xit", "Exit") + self.menuFileSaveAs = self.menuFile.Append(wx.ID_SAVE, "Save &As...", "Save As...") + self.menuBar = wx.MenuBar() + self.menuBar.Append(self.menuFile, "&File") + + self.panelSkin = wx.Panel(self, wx.ID_ANY) + self.panelCanvas = MiRCARTCanvas(self.panelSkin, \ + canvasPos=canvasPos, cellSize=cellSize, canvasSize=canvasSize) + + self.Bind(wx.EVT_MENU, self.onFileExit, self.menuFileExit) + self.Bind(wx.EVT_MENU, self.onFileSaveAs, self.menuFileSaveAs) + self.CreateStatusBar() + self.SetMenuBar(self.menuBar) + self.Show(True) # # Entry point def main(*argv): - _MiRCART = MiRCART(*argv[1:]) + wxApp = wx.App(False) + MiRCARTFrame(None) + wxApp.MainLoop() if __name__ == "__main__": - if ((len(sys.argv) - 1) < 2)\ - or ((len(sys.argv) - 1) > 4): - print("usage: {} " \ - " " \ - " " \ - "[] " \ - "[]".format(sys.argv[0]), file=sys.stderr) - else: - main(*sys.argv) + main(*sys.argv) -# vim:expandtab foldmethod=marker sw=8 ts=8 tw=120 +# vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/README.md b/README.md index 6239374..cfde2b4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,10 @@ -# mirc2png -- convert ASCII w/ mIRC control codes to monospaced PNG (for EFnet #MiRCART) -* Prerequisites: python3 && python3-pil (&& python3-{json,requests,urllib3} for IrcMiRCARTBot.py) on Debian-family Linux distributions +# MiRCART -- XXX +* Prerequisites: python3 && python-wx{gtk2.8,tools} on Debian-family Linux distributions + +# IrcMiRCARTBot.py -- XXX +* Prerequisites: python3 && python3-{json,requests,urllib3} on Debian-family Linux distributions * IrcMiRCARTBot.py usage: IrcMiRCARTBot.py `` [``] [``] [``] [``] [``] -* MiRCART.py usage: MiRCART.py `` `` [``] [``] + +# MiRC2png.py -- convert ASCII w/ mIRC control codes to monospaced PNG (for EFnet #MiRCART) +* Prerequisites: python3 && python3-pil on Debian-family Linux distributions +* MiRC2png.py usage: MiRC2png.py `` `` [``] [``] From 427290e7831a95dd49911df016c99ec11ef0137b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Thu, 4 Jan 2018 17:26:12 +0100 Subject: [PATCH 042/148] MiRCART.py:mircColours, MiRCARTCanvas.{onPaint,__init__}(): moved from MiRCARTCanvas.mircColours. MiRCART.py:MiRCARTCanvas.get{Background,Foreground}Colour: added for MiRCARTFrame._updateStatusBar(). MiRCART.py:MiRCARTCanvas.onCharHook(): merged into MiRCARTCanvas.onPaletteEvent(). MiRCART.py:MiRCARTCanvas.onPaletteEvent(): set self.{mirc,brush,pen}{Fg,Bg} from colour choice event. MiRCART.py:MiRCARTPalette: implements 16 colour palette panels GUI. MiRCART.py:MiRCARTFrame._updateStatusBar(): show canvas {fore,back}ground colours in status bar. MiRCART.py:MiRCARTFrame.onPaletteEvent(): hand off to MiRCARTCanvas.onPaletteEvent() and call _updateStatusBar(). MiRCART.py:MiRCARTFrame.__init__(): fix `&File' menu item order. MiRCART.py:MiRCARTFrame.__init__(): create & update status bar. --- MiRCART.py | 158 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 110 insertions(+), 48 deletions(-) diff --git a/MiRCART.py b/MiRCART.py index 3cf69b8..fdc6804 100755 --- a/MiRCART.py +++ b/MiRCART.py @@ -25,6 +25,27 @@ import wx import sys +# {{{ mircColours: mIRC colour number to RGBA map given none of ^[BFV_] (bold, italic, reverse, underline) +mircColours = [ + (255, 255, 255, 255), # White + (0, 0, 0, 255), # Black + (0, 0, 187, 255), # Blue + (0, 187, 0, 255), # Green + (255, 85, 85, 255), # Light Red + (187, 0, 0, 255), # Red + (187, 0, 187, 255), # Purple + (187, 187, 0, 255), # Yellow + (255, 255, 85, 255), # Light Yellow + (85, 255, 85, 255), # Light Green + (0, 187, 187, 255), # Cyan + (85, 255, 255, 255), # Light Cyan + (85, 85, 255, 255), # Light Blue + (255, 85, 255, 255), # Pink + (85, 85, 85, 255), # Grey + (187, 187, 187, 255), # Light Grey +] +# }}} + class MiRCARTCanvas(wx.Panel): """XXX""" canvasPos = canvasSize = None @@ -32,26 +53,6 @@ class MiRCARTCanvas(wx.Panel): cellPos = cellSize = None brushBg = brushFg = penBg = penFg = None mircBg = mircFg = None - # {{{ mircColours: mIRC colour number to RGBA map given none of ^[BFV_] (bold, italic, reverse, underline) - mircColours = [ - (255, 255, 255, 255), # White - (0, 0, 0, 255), # Black - (0, 0, 187, 255), # Blue - (0, 187, 0, 255), # Green - (255, 85, 85, 255), # Light Red - (187, 0, 0, 255), # Red - (187, 0, 187, 255), # Purple - (187, 187, 0, 255), # Yellow - (255, 255, 85, 255), # Light Yellow - (85, 255, 85, 255), # Light Green - (0, 187, 187, 255), # Cyan - (85, 255, 255, 255), # Light Cyan - (85, 85, 255, 255), # Light Blue - (255, 85, 255, 255), # Pink - (85, 85, 85, 255), # Grey - (187, 187, 187, 255), # Light Grey - ] - # }}} # {{{ _onMouseEvent(): XXX def _onMouseEvent(self, event): @@ -72,20 +73,21 @@ class MiRCARTCanvas(wx.Panel): eventDc.SetBrush(self.brushBg); eventDc.SetPen(self.penBg) self.canvasMap[mapX][mapY] = [self.mircBg, self.mircBg, " "] - eventDc.DrawRectangle(rectX, rectY, \ + eventDc.DrawRectangle(rectX, rectY, \ self.cellSize[0], self.cellSize[1]) # }}} + # {{{ getBackgroundColour(): XXX + def getBackgroundColour(self): + return self.mircBg + # }}} + # {{{ getForegroundColour(): XXX + def getForegroundColour(self): + return self.mircFg + # }}} # {{{ onCharHook(): XXX def onCharHook(self, event): keyCode = event.GetKeyCode() - if keyCode == wx.WXK_UP: - self.mircFg = self.mircFg + 1 if self.mircFg < 15 else 15 - elif keyCode == wx.WXK_DOWN: - self.mircFg = self.mircFg - 1 if self.mircFg > 0 else 0 - self.brushBg = wx.Brush(wx.Colour(self.mircColours[self.mircBg]), wx.BRUSHSTYLE_SOLID) - self.brushFg = wx.Brush(wx.Colour(self.mircColours[self.mircFg]), wx.BRUSHSTYLE_SOLID) - self.penBg = wx.Pen(wx.Colour(self.mircColours[self.mircBg]), 1) - self.penFg = wx.Pen(wx.Colour(self.mircColours[self.mircFg]), 1) + pass # }}} # {{{ onLeftDown(): XXX def onLeftDown(self, event): @@ -102,32 +104,41 @@ class MiRCARTCanvas(wx.Panel): eventDc.Clear() for cellX in range(0, self.canvasSize[0]): for cellY in range(0, self.canvasSize[1]): - eventDc.SetBackground(wx.Brush(wx.Colour(self.mircColours[self.canvasMap[cellX][cellY][0]]), wx.BRUSHSTYLE_SOLID)) - eventDc.SetBrush(wx.Brush(wx.Colour(self.mircColours[self.canvasMap[cellX][cellY][1]]), wx.BRUSHSTYLE_SOLID)) - eventDc.SetPen(wx.Pen(wx.Colour(self.mircColours[self.canvasMap[cellX][cellY][1]]), 1)) + eventDc.SetBackground(wx.Brush(wx.Colour(mircColours[self.canvasMap[cellX][cellY][0]]), wx.BRUSHSTYLE_SOLID)) + eventDc.SetBrush(wx.Brush(wx.Colour(mircColours[self.canvasMap[cellX][cellY][1]]), wx.BRUSHSTYLE_SOLID)) + eventDc.SetPen(wx.Pen(wx.Colour(mircColours[self.canvasMap[cellX][cellY][1]]), 1)) rectX = cellX * self.cellSize[0]; rectY = cellY * self.cellSize[1]; - eventDc.DrawRectangle(rectX, rectY, \ + eventDc.DrawRectangle(rectX, rectY, \ self.cellSize[0], self.cellSize[1]) # }}} + # {{{ onPaletteEvent(): XXX + def onPaletteEvent(self, leftDown, rightDown, numColour): + if leftDown: + self.mircFg = numColour + self.brushFg = wx.Brush(wx.Colour(mircColours[self.mircFg]), wx.BRUSHSTYLE_SOLID) + self.penFg = wx.Pen(wx.Colour(mircColours[self.mircFg]), 1) + elif rightDown: + self.mircBg = numColour + self.brushBg = wx.Brush(wx.Colour(mircColours[self.mircBg]), wx.BRUSHSTYLE_SOLID) + self.penBg = wx.Pen(wx.Colour(mircColours[self.mircBg]), 1) + # }}} # {{{ onRightDown(): XXX def onRightDown(self, event): self._onMouseEvent(event) # }}} - - # - # Initialisation method + # {{{ Initialisation method def __init__(self, parent, canvasPos, cellSize, canvasSize): - super().__init__(parent, pos=canvasPos, size=( \ + super().__init__(parent, pos=canvasPos, size=( \ cellSize[0] * canvasSize[0], cellSize[1] * canvasSize[1])) self.canvasPos = canvasPos; self.canvasSize = canvasSize; self.canvasMap = [[[1, 1, " "] for y in range(canvasSize[1])] for x in range(canvasSize[0])] self.cellPos = (0, 0); self.cellSize = cellSize; - self.brushBg = wx.Brush(wx.Colour(self.mircColours[1]), wx.BRUSHSTYLE_SOLID) - self.brushFg = wx.Brush(wx.Colour(self.mircColours[4]), wx.BRUSHSTYLE_SOLID) - self.penBg = wx.Pen(wx.Colour(self.mircColours[1]), 1) - self.penFg = wx.Pen(wx.Colour(self.mircColours[4]), 1) + self.brushBg = wx.Brush(wx.Colour(mircColours[1]), wx.BRUSHSTYLE_SOLID) + self.brushFg = wx.Brush(wx.Colour(mircColours[4]), wx.BRUSHSTYLE_SOLID) + self.penBg = wx.Pen(wx.Colour(mircColours[1]), 1) + self.penFg = wx.Pen(wx.Colour(mircColours[4]), 1) self.mircBg = 1; self.mircFg = 4; self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM) @@ -136,12 +147,55 @@ class MiRCARTCanvas(wx.Panel): self.Bind(wx.EVT_MOTION, self.onMotion) self.Bind(wx.EVT_PAINT, self.onPaint) self.Bind(wx.EVT_RIGHT_DOWN, self.onRightDown) + # }}} + +class MiRCARTPalette(wx.Panel): + """XXX""" + panelsByColour = onPaletteEvent = None + + # {{{ onLeftDown(): XXX + def onLeftDown(self, event): + numColour = int(event.GetEventObject().GetName()) + self.onPaletteEvent(True, False, numColour) + # }}} + # {{{ onRightDown(): XXX + def onRightDown(self, event): + numColour = int(event.GetEventObject().GetName()) + self.onPaletteEvent(False, True, numColour) + # }}} + # {{{ Initialisation method + def __init__(self, parent, parentPos, cellSize, onPaletteEvent): + panelSizeW = 6 * cellSize[0]; panelSizeH = 2 * cellSize[1]; + paletteSize = (panelSizeW * 16, panelSizeH) + super().__init__(parent, pos=parentPos, size=paletteSize) + self.panelsByColour = [None] * len(mircColours) + for numColour in range(0, len(mircColours)): + posX = (numColour * (cellSize[0] * 6)) + self.panelsByColour[numColour] = wx.Panel(self, \ + pos=(posX, 0), size=(panelSizeW, panelSizeH)) + self.panelsByColour[numColour].SetBackgroundColour( \ + wx.Colour(mircColours[numColour])) + self.panelsByColour[numColour].Bind(wx.EVT_LEFT_DOWN, self.onLeftDown) + self.panelsByColour[numColour].Bind(wx.EVT_RIGHT_DOWN, self.onRightDown) + self.panelsByColour[numColour].SetName(str(numColour)) + self.onPaletteEvent = onPaletteEvent + # }}} class MiRCARTFrame(wx.Frame): """XXX""" menuFile = menuFileSaveAs = menuFileExit = menuBar = None - panelSkin = panelCanvas = None + panelSkin = panelCanvas = panelPalette = None + statusBar = None + # {{{ _updateStatusBar(): XXX + def _updateStatusBar(self): + text = "Foreground colour:" + text += " " + str(self.panelCanvas.getForegroundColour()) + text += " | " + text += "Background colour:" + text += " " + str(self.panelCanvas.getBackgroundColour()) + self.statusBar.SetStatusText(text) + # }}} # {{{ onFileSaveAs(): XXX def onFileSaveAs(self, event): pass @@ -150,27 +204,35 @@ class MiRCARTFrame(wx.Frame): def onFileExit(self, event): self.Close(True) # }}} - - # - # Initialisation method + # {{{ onPaletteEvent(): XXX + def onPaletteEvent(self, leftDown, rightDown, numColour): + self.panelCanvas.onPaletteEvent(leftDown, rightDown, numColour) + self._updateStatusBar() + # }}} + # {{{ Initialisation method def __init__(self, parent, appSize=(1024, 768), canvasPos=(25, 25), cellSize=(7, 14), canvasSize=(80, 25)): super().__init__(parent, wx.ID_ANY, "MiRCART", size=appSize) self.menuFile = wx.Menu() - self.menuFileExit = self.menuFile.Append(wx.ID_EXIT, "E&xit", "Exit") self.menuFileSaveAs = self.menuFile.Append(wx.ID_SAVE, "Save &As...", "Save As...") + self.menuFileExit = self.menuFile.Append(wx.ID_EXIT, "E&xit", "Exit") self.menuBar = wx.MenuBar() self.menuBar.Append(self.menuFile, "&File") self.panelSkin = wx.Panel(self, wx.ID_ANY) - self.panelCanvas = MiRCARTCanvas(self.panelSkin, \ + self.panelCanvas = MiRCARTCanvas(self.panelSkin, \ canvasPos=canvasPos, cellSize=cellSize, canvasSize=canvasSize) + self.panelPalette = MiRCARTPalette(self.panelSkin, \ + (25, (canvasSize[1] + 3) * cellSize[1]), cellSize, self.onPaletteEvent) + + self.statusBar = self.CreateStatusBar() + self._updateStatusBar() self.Bind(wx.EVT_MENU, self.onFileExit, self.menuFileExit) self.Bind(wx.EVT_MENU, self.onFileSaveAs, self.menuFileSaveAs) - self.CreateStatusBar() self.SetMenuBar(self.menuBar) self.Show(True) + # }}} # # Entry point From 42e18ec252ce0c4ef105025d0ace09a5630fd9ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Thu, 4 Jan 2018 18:11:57 +0100 Subject: [PATCH 043/148] MiRCART.py:MiRCARTCanvas.{_onMouseEvent,onPaint,__init__}(): switch addressing mode of self.canvasMap to [Y][X]. MiRCART.py:MiRCARTCanvas.get{Height,Map,Width}(): added for MiRCARTFrame.onFileSaveAs(). MiRCART.py:MiRCARTFrame.onFileSaveAs(): initial implementation. --- MiRCART.py | 51 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/MiRCART.py b/MiRCART.py index fdc6804..3f02006 100755 --- a/MiRCART.py +++ b/MiRCART.py @@ -23,7 +23,7 @@ # import wx -import sys +import os, sys # {{{ mircColours: mIRC colour number to RGBA map given none of ^[BFV_] (bold, italic, reverse, underline) mircColours = [ @@ -68,11 +68,11 @@ class MiRCARTCanvas(wx.Panel): if event.LeftIsDown(): eventDc.SetBrush(self.brushFg); eventDc.SetPen(self.penFg) - self.canvasMap[mapX][mapY] = [self.mircFg, self.mircFg, " "] + self.canvasMap[mapY][mapX] = [self.mircFg, self.mircFg, " "] elif event.RightIsDown(): eventDc.SetBrush(self.brushBg); eventDc.SetPen(self.penBg) - self.canvasMap[mapX][mapY] = [self.mircBg, self.mircBg, " "] + self.canvasMap[mapY][mapX] = [self.mircBg, self.mircBg, " "] eventDc.DrawRectangle(rectX, rectY, \ self.cellSize[0], self.cellSize[1]) # }}} @@ -84,6 +84,18 @@ class MiRCARTCanvas(wx.Panel): def getForegroundColour(self): return self.mircFg # }}} + # {{{ getHeight(): XXX + def getHeight(self): + return self.canvasSize[1] + # }}} + # {{{ getMap(): XXX + def getMap(self): + return self.canvasMap + # }}} + # {{{ getWidth(): XXX + def getWidth(self): + return self.canvasSize[0] + # }}} # {{{ onCharHook(): XXX def onCharHook(self, event): keyCode = event.GetKeyCode() @@ -104,9 +116,9 @@ class MiRCARTCanvas(wx.Panel): eventDc.Clear() for cellX in range(0, self.canvasSize[0]): for cellY in range(0, self.canvasSize[1]): - eventDc.SetBackground(wx.Brush(wx.Colour(mircColours[self.canvasMap[cellX][cellY][0]]), wx.BRUSHSTYLE_SOLID)) - eventDc.SetBrush(wx.Brush(wx.Colour(mircColours[self.canvasMap[cellX][cellY][1]]), wx.BRUSHSTYLE_SOLID)) - eventDc.SetPen(wx.Pen(wx.Colour(mircColours[self.canvasMap[cellX][cellY][1]]), 1)) + eventDc.SetBackground(wx.Brush(wx.Colour(mircColours[self.canvasMap[cellY][cellX][0]]), wx.BRUSHSTYLE_SOLID)) + eventDc.SetBrush(wx.Brush(wx.Colour(mircColours[self.canvasMap[cellY][cellX][1]]), wx.BRUSHSTYLE_SOLID)) + eventDc.SetPen(wx.Pen(wx.Colour(mircColours[self.canvasMap[cellY][cellX][1]]), 1)) rectX = cellX * self.cellSize[0]; rectY = cellY * self.cellSize[1]; eventDc.DrawRectangle(rectX, rectY, \ self.cellSize[0], self.cellSize[1]) @@ -133,7 +145,7 @@ class MiRCARTCanvas(wx.Panel): cellSize[1] * canvasSize[1])) self.canvasPos = canvasPos; self.canvasSize = canvasSize; - self.canvasMap = [[[1, 1, " "] for y in range(canvasSize[1])] for x in range(canvasSize[0])] + self.canvasMap = [[[1, 1, " "] for x in range(canvasSize[0])] for y in range(canvasSize[1])] self.cellPos = (0, 0); self.cellSize = cellSize; self.brushBg = wx.Brush(wx.Colour(mircColours[1]), wx.BRUSHSTYLE_SOLID) self.brushFg = wx.Brush(wx.Colour(mircColours[4]), wx.BRUSHSTYLE_SOLID) @@ -198,7 +210,30 @@ class MiRCARTFrame(wx.Frame): # }}} # {{{ onFileSaveAs(): XXX def onFileSaveAs(self, event): - pass + with wx.FileDialog(self, "Save As...", os.getcwd(), "", \ + "*.txt", wx.FD_SAVE|wx.FD_OVERWRITE_PROMPT) as dialog: + if dialog.ShowModal() == wx.ID_CANCEL: + return + else: + try: + with open(dialog.GetPath(), "w") as file: + canvasMap = self.panelCanvas.getMap() + canvasHeight = self.panelCanvas.getHeight() + canvasWidth = self.panelCanvas.getWidth() + for canvasRow in range(0, canvasHeight): + colourLastBg = colourLastFg = None; + for canvasCol in range(0, canvasWidth): + canvasColBg = canvasMap[canvasRow][canvasCol][0] + canvasColFg = canvasMap[canvasRow][canvasCol][1] + canvasColText = canvasMap[canvasRow][canvasCol][2] + if colourLastBg != canvasColBg \ + or colourLastFg != canvasColFg: + colourLastBg = canvasColBg; colourLastFg = canvasColFg; + file.write("" + str(canvasColFg) + "," + str(canvasColBg)) + file.write(canvasColText) + file.write("\n") + except IOError as error: + wx.LogError("IOError {}".format(error)) # }}} # {{{ onFileExit(): XXX def onFileExit(self, event): From dcb4ca83bb99cbc8fd07c25ea99374cb25f1b17f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Thu, 4 Jan 2018 20:13:38 +0100 Subject: [PATCH 044/148] MiRCART.py:MiRCARTCanvas.{onCharHook,__init__}(): removes wx.EVT_CHAR_HOOK event handler. --- MiRCART.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/MiRCART.py b/MiRCART.py index 3f02006..29cb75a 100755 --- a/MiRCART.py +++ b/MiRCART.py @@ -96,11 +96,6 @@ class MiRCARTCanvas(wx.Panel): def getWidth(self): return self.canvasSize[0] # }}} - # {{{ onCharHook(): XXX - def onCharHook(self, event): - keyCode = event.GetKeyCode() - pass - # }}} # {{{ onLeftDown(): XXX def onLeftDown(self, event): self._onMouseEvent(event) @@ -154,7 +149,6 @@ class MiRCARTCanvas(wx.Panel): self.mircBg = 1; self.mircFg = 4; self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM) - self.Bind(wx.EVT_CHAR_HOOK, self.onCharHook) self.Bind(wx.EVT_LEFT_DOWN, self.onLeftDown) self.Bind(wx.EVT_MOTION, self.onMotion) self.Bind(wx.EVT_PAINT, self.onPaint) From 91e33f8a4a87ed56dea292385048cd70db7eb338 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Thu, 4 Jan 2018 20:22:15 +0100 Subject: [PATCH 045/148] MiRCART.py:MiRCARTCanvas.canvasBitmap: added to optimise onPaint(). MiRCART.py:MiRCARTCanvas._onMouseEvent(): additionally draw to canvasBitmap via temporary wx.MemoryDC(). MiRCART.py:MiRCARTCanvas.onPaint(): reimplement using (double-buffered) wx.BufferedPaintDC() from canvasBitmap. MiRCART.py:MiRCARTCanvas.__init__(): initialise canvasBitmap from canvas window size. --- MiRCART.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/MiRCART.py b/MiRCART.py index 29cb75a..90cc980 100755 --- a/MiRCART.py +++ b/MiRCART.py @@ -49,7 +49,7 @@ mircColours = [ class MiRCARTCanvas(wx.Panel): """XXX""" canvasPos = canvasSize = None - canvasMap = None + canvasBitmap = canvasMap = None cellPos = cellSize = None brushBg = brushFg = penBg = penFg = None mircBg = mircFg = None @@ -59,22 +59,31 @@ class MiRCARTCanvas(wx.Panel): eventObject = event.GetEventObject() if event.Dragging(): eventDc = wx.ClientDC(self) + tmpDc = wx.MemoryDC() + tmpDc.SelectObject(self.canvasBitmap) eventPoint = event.GetLogicalPosition(eventDc) rectX = eventPoint.x - (eventPoint.x % self.cellSize[0]) rectY = eventPoint.y - (eventPoint.y % self.cellSize[1]) mapX = int(rectX / 7 if rectX else 0) mapY = int(rectY / 14 if rectY else 0) eventDc.SetBackground(self.brushBg); + tmpDc.SetBackground(self.brushBg); if event.LeftIsDown(): eventDc.SetBrush(self.brushFg); eventDc.SetPen(self.penFg) + tmpDc.SetBrush(self.brushFg); + tmpDc.SetPen(self.penFg) self.canvasMap[mapY][mapX] = [self.mircFg, self.mircFg, " "] elif event.RightIsDown(): eventDc.SetBrush(self.brushBg); eventDc.SetPen(self.penBg) + tmpDc.SetBrush(self.brushBg); + tmpDc.SetPen(self.penBg) self.canvasMap[mapY][mapX] = [self.mircBg, self.mircBg, " "] eventDc.DrawRectangle(rectX, rectY, \ self.cellSize[0], self.cellSize[1]) + tmpDc.DrawRectangle(rectX, rectY, \ + self.cellSize[0], self.cellSize[1]) # }}} # {{{ getBackgroundColour(): XXX def getBackgroundColour(self): @@ -106,17 +115,7 @@ class MiRCARTCanvas(wx.Panel): # }}} # {{{ onPaint(): XXX def onPaint(self, event): - eventDc = wx.BufferedPaintDC(self) - eventDc.SetBackground(wx.Brush(wx.BLACK)) - eventDc.Clear() - for cellX in range(0, self.canvasSize[0]): - for cellY in range(0, self.canvasSize[1]): - eventDc.SetBackground(wx.Brush(wx.Colour(mircColours[self.canvasMap[cellY][cellX][0]]), wx.BRUSHSTYLE_SOLID)) - eventDc.SetBrush(wx.Brush(wx.Colour(mircColours[self.canvasMap[cellY][cellX][1]]), wx.BRUSHSTYLE_SOLID)) - eventDc.SetPen(wx.Pen(wx.Colour(mircColours[self.canvasMap[cellY][cellX][1]]), 1)) - rectX = cellX * self.cellSize[0]; rectY = cellY * self.cellSize[1]; - eventDc.DrawRectangle(rectX, rectY, \ - self.cellSize[0], self.cellSize[1]) + eventDc = wx.BufferedPaintDC(self, self.canvasBitmap) # }}} # {{{ onPaletteEvent(): XXX def onPaletteEvent(self, leftDown, rightDown, numColour): @@ -135,11 +134,12 @@ class MiRCARTCanvas(wx.Panel): # }}} # {{{ Initialisation method def __init__(self, parent, canvasPos, cellSize, canvasSize): - super().__init__(parent, pos=canvasPos, size=( \ - cellSize[0] * canvasSize[0], - cellSize[1] * canvasSize[1])) + winSizeW = cellSize[0] * canvasSize[0] + winSizeH = cellSize[1] * canvasSize[1] + super().__init__(parent, pos=canvasPos, size=(winSizeW, winSizeH)) self.canvasPos = canvasPos; self.canvasSize = canvasSize; + self.canvasBitmap = wx.Bitmap(winSizeW, winSizeH) self.canvasMap = [[[1, 1, " "] for x in range(canvasSize[0])] for y in range(canvasSize[1])] self.cellPos = (0, 0); self.cellSize = cellSize; self.brushBg = wx.Brush(wx.Colour(mircColours[1]), wx.BRUSHSTYLE_SOLID) From 325a454db71e10a42408982dbfdc56e19020ee26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Thu, 4 Jan 2018 23:22:09 +0100 Subject: [PATCH 046/148] MiRCART.py:MiRCARTCanvas._eventPointToMap[XY](): added for _onMouseEvent(). MiRCART.py:MiRCARTCanvas._onMouseEvent(): split into _processMapPatches() and _drawPatch(). MiRCART.py:MiRCARTCanvas.getCell{Height,Width}(): added for _drawPatch() and _eventPointToMap[XY](). MiRCART.py:MiRCARTCanvas.onPaletteEvent(): only set self.mirc[FB]g. MiRCART.py:MiRCARTCanvas.__init__(): cache canvas window size. MiRCART.py:MiRCARTCanvas.__init__(): initialise & append canvasTools. MiRCART.py:MiRCARTCanvas.__init__(): pre-create 16 mIRC colour brushes & pens. MiRCART.py:MiRCARTCanvas.__init__(): initialise temporary & {un,re}do patches. MiRCART.py:MiRCARTTool: added abstract base class for all canvas tools. MiRCART.py:MiRCARTToolRect: initial implementation w/ cursor, limited to 1x1 rectangles. MiRCART.py:MiRCARTFrame.__init__(): initialise MiRCARTCanvas() w/ canvasTools=[MiRCARTToolRect]. --- MiRCART.py | 206 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 150 insertions(+), 56 deletions(-) diff --git a/MiRCART.py b/MiRCART.py index 90cc980..e0d7c91 100755 --- a/MiRCART.py +++ b/MiRCART.py @@ -48,47 +48,79 @@ mircColours = [ class MiRCARTCanvas(wx.Panel): """XXX""" - canvasPos = canvasSize = None - canvasBitmap = canvasMap = None - cellPos = cellSize = None - brushBg = brushFg = penBg = penFg = None - mircBg = mircFg = None + canvasPos = canvasSize = canvasWinSize = cellPos = cellSize = None + canvasBitmap = canvasMap = canvasTools = None + mircBg = mircFg = mircBrushes = mircPens = None + patchesTmp = patchesUndo = None + # {{{ _drawPatch(): XXX + def _drawPatch(self, patch, eventDc, tmpDc, atX, atY): + patchXabs = (atX + patch[0]) * self.getCellWidth() + patchYabs = (atY + patch[1]) * self.getCellHeight() + brushFg = self.mircBrushes[patch[2]] + brushBg = self.mircBrushes[patch[3]] + pen = self.mircPens[patch[2]] + for dc in (eventDc, tmpDc): + dc.SetBrush(brushFg); dc.SetBackground(brushBg); dc.SetPen(pen); + dc.DrawRectangle(patchXabs, patchYabs, \ + self.getCellWidth(), self.getCellHeight()) + # }}} + # {{{ _eventPointToMapX(): XXX + def _eventPointToMapX(self, eventPoint): + rectX = eventPoint.x - (eventPoint.x % self.getCellWidth()) + return int(rectX / self.getCellWidth() if rectX else 0) + # }}} + # {{{ _eventPointToMapY(): XXX + def _eventPointToMapY(self, eventPoint): + rectY = eventPoint.y - (eventPoint.y % self.getCellHeight()) + return int(rectY / self.getCellHeight() if rectY else 0) + # }}} # {{{ _onMouseEvent(): XXX def _onMouseEvent(self, event): eventObject = event.GetEventObject() - if event.Dragging(): - eventDc = wx.ClientDC(self) - tmpDc = wx.MemoryDC() - tmpDc.SelectObject(self.canvasBitmap) - eventPoint = event.GetLogicalPosition(eventDc) - rectX = eventPoint.x - (eventPoint.x % self.cellSize[0]) - rectY = eventPoint.y - (eventPoint.y % self.cellSize[1]) - mapX = int(rectX / 7 if rectX else 0) - mapY = int(rectY / 14 if rectY else 0) - eventDc.SetBackground(self.brushBg); - tmpDc.SetBackground(self.brushBg); - if event.LeftIsDown(): - eventDc.SetBrush(self.brushFg); - eventDc.SetPen(self.penFg) - tmpDc.SetBrush(self.brushFg); - tmpDc.SetPen(self.penFg) - self.canvasMap[mapY][mapX] = [self.mircFg, self.mircFg, " "] - elif event.RightIsDown(): - eventDc.SetBrush(self.brushBg); - eventDc.SetPen(self.penBg) - tmpDc.SetBrush(self.brushBg); - tmpDc.SetPen(self.penBg) - self.canvasMap[mapY][mapX] = [self.mircBg, self.mircBg, " "] - eventDc.DrawRectangle(rectX, rectY, \ - self.cellSize[0], self.cellSize[1]) - tmpDc.DrawRectangle(rectX, rectY, \ - self.cellSize[0], self.cellSize[1]) + eventDc = wx.ClientDC(self); tmpDc = wx.MemoryDC(); + tmpDc.SelectObject(self.canvasBitmap) + eventPoint = event.GetLogicalPosition(eventDc) + mapX = self._eventPointToMapX(eventPoint) + mapY = self._eventPointToMapY(eventPoint) + for tool in self.canvasTools: + if event.Dragging(): + mapPatches = tool.onMouseMotion(event, mapX, mapY, event.LeftIsDown(), event.RightIsDown()) + else: + mapPatches = tool.onMouseDown(event, mapX, mapY, event.LeftIsDown(), event.RightIsDown()) + self._processMapPatches(mapPatches, eventDc, tmpDc, mapX, mapY) + # }}} + # {{{ _processMapPatches(): XXX + def _processMapPatches(self, mapPatches, eventDc, tmpDc, atX, atY): + for mapPatch in mapPatches: + mapPatchTmp = mapPatch[0]; mapPatchW = mapPatch[1]; mapPatchH = mapPatch[2]; + if mapPatchTmp and self.patchesTmp: + for patch in self.patchesTmp: + self._drawPatch(patch, eventDc, tmpDc, 0, 0) + self.patchesTmp = [] + for patch in mapPatch[3]: + if mapPatchTmp: + mapItem = self.canvasMap[atY + patch[1]][atX + patch[0]] + self.patchesTmp.append((atX + patch[0], \ + atY + patch[1], mapItem[0], mapItem[1], mapItem[2])) + self._drawPatch(patch, eventDc, tmpDc, atX, atY) + else: + self._drawPatch(patch, eventDc, tmpDc, atX, atY) + self.canvasMap[atY + patch[1]][atX + patch[0]] =\ + [patch[2], patch[3], " "]; # }}} # {{{ getBackgroundColour(): XXX def getBackgroundColour(self): return self.mircBg # }}} + # {{{ getCellHeight(): XXX + def getCellHeight(self): + return self.cellSize[1] + # }}} + # {{{ getCellWidth(): XXX + def getCellWidth(self): + return self.cellSize[0] + # }}} # {{{ getForegroundColour(): XXX def getForegroundColour(self): return self.mircFg @@ -121,33 +153,36 @@ class MiRCARTCanvas(wx.Panel): def onPaletteEvent(self, leftDown, rightDown, numColour): if leftDown: self.mircFg = numColour - self.brushFg = wx.Brush(wx.Colour(mircColours[self.mircFg]), wx.BRUSHSTYLE_SOLID) - self.penFg = wx.Pen(wx.Colour(mircColours[self.mircFg]), 1) elif rightDown: self.mircBg = numColour - self.brushBg = wx.Brush(wx.Colour(mircColours[self.mircBg]), wx.BRUSHSTYLE_SOLID) - self.penBg = wx.Pen(wx.Colour(mircColours[self.mircBg]), 1) # }}} # {{{ onRightDown(): XXX def onRightDown(self, event): self._onMouseEvent(event) # }}} # {{{ Initialisation method - def __init__(self, parent, canvasPos, cellSize, canvasSize): - winSizeW = cellSize[0] * canvasSize[0] - winSizeH = cellSize[1] * canvasSize[1] - super().__init__(parent, pos=canvasPos, size=(winSizeW, winSizeH)) - - self.canvasPos = canvasPos; self.canvasSize = canvasSize; - self.canvasBitmap = wx.Bitmap(winSizeW, winSizeH) - self.canvasMap = [[[1, 1, " "] for x in range(canvasSize[0])] for y in range(canvasSize[1])] + def __init__(self, parent, canvasPos, cellSize, canvasSize, canvasTools): + canvasWinSize = (cellSize[0] * canvasSize[0], cellSize[1] * canvasSize[1]) + super().__init__(parent, pos=canvasPos, size=canvasWinSize) + self.canvasPos = canvasPos; self.canvasSize = canvasSize; self.canvasWinSize = canvasWinSize; self.cellPos = (0, 0); self.cellSize = cellSize; - self.brushBg = wx.Brush(wx.Colour(mircColours[1]), wx.BRUSHSTYLE_SOLID) - self.brushFg = wx.Brush(wx.Colour(mircColours[4]), wx.BRUSHSTYLE_SOLID) - self.penBg = wx.Pen(wx.Colour(mircColours[1]), 1) - self.penFg = wx.Pen(wx.Colour(mircColours[4]), 1) + + self.canvasBitmap = wx.Bitmap(canvasWinSize) + self.canvasMap = [[[1, 1, " "] for x in range(canvasSize[0])] for y in range(canvasSize[1])] + self.canvasTools = [] + for canvasTool in canvasTools: + self.canvasTools.append(canvasTool(self)) + self.mircBg = 1; self.mircFg = 4; - self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM) + self.mircBrushes = [None for x in range(len(mircColours))] + self.mircPens = [None for x in range(len(mircColours))] + for mircColour in range(0, len(mircColours)): + self.mircBrushes[mircColour] = wx.Brush( \ + wx.Colour(mircColours[mircColour]), wx.BRUSHSTYLE_SOLID) + self.mircPens[mircColour] = wx.Pen( \ + wx.Colour(mircColours[mircColour]), 1) + + self.patchesTmp = []; self.patchesUndo = []; self.Bind(wx.EVT_LEFT_DOWN, self.onLeftDown) self.Bind(wx.EVT_MOTION, self.onMotion) @@ -155,6 +190,64 @@ class MiRCARTCanvas(wx.Panel): self.Bind(wx.EVT_RIGHT_DOWN, self.onRightDown) # }}} +class MiRCARTTool(): + """XXX""" + parentCanvas = None + + # {{{ onMouseDown(): XXX + def onMouseDown(self, event, mapX, mapY, isLeftDown, isRightDown): + pass + # }}} + # {{{ onMouseMotion(): XXX + def onMouseMotion(self, event, mapX, mapY, isLeftDown, isRightDown): + pass + # }}} + # {{{ Initialisation method + def __init__(self, parentCanvas): + self.parentCanvas = parentCanvas + # }}} + +class MiRCARTToolRect(MiRCARTTool): + """XXX""" + + # {{{ _draw(): XXX + def _draw(self, event, mapX, mapY, isLeftDown, isRightDown): + if isLeftDown: + return [[False, 1, 1, [[0, 0, \ + self.parentCanvas.getForegroundColour(), \ + self.parentCanvas.getForegroundColour(), " "]]], + [True, 1, 1, [[0, 0, \ + self.parentCanvas.getForegroundColour(), \ + self.parentCanvas.getForegroundColour(), " "]]]] + elif isRightDown: + return [[False, 1, 1, [[0, 0, \ + self.parentCanvas.getBackgroundColour(), \ + self.parentCanvas.getBackgroundColour(), " "]]], \ + [True, 1, 1, [[0, 0, \ + self.parentCanvas.getForegroundColour(), \ + self.parentCanvas.getForegroundColour(), " "]]]] + else: + return [[True, 1, 1, [[0, 0, \ + self.parentCanvas.getForegroundColour(), \ + self.parentCanvas.getForegroundColour(), " "]]]] + # }}} + # {{{ onMouseDown(): XXX + def onMouseDown(self, event, mapX, mapY, isLeftDown, isRightDown): + print("mouse down") + return self._draw(event, mapX, mapY, isLeftDown, isRightDown) + # }}} + # {{{ onMouseMotion(): XXX + def onMouseMotion(self, event, mapX, mapY, isLeftDown, isRightDown): + print("mouse moving") + if isLeftDown or isRightDown: + print("mouse depressed") + return self._draw(event, mapX, mapY, isLeftDown, isRightDown) + # }}} + # {{{ Initialisation method + def __init__(self, parentCanvas): + super().__init__(parentCanvas) + # }}} + class MiRCARTPalette(wx.Panel): """XXX""" panelsByColour = onPaletteEvent = None @@ -177,9 +270,9 @@ class MiRCARTPalette(wx.Panel): self.panelsByColour = [None] * len(mircColours) for numColour in range(0, len(mircColours)): posX = (numColour * (cellSize[0] * 6)) - self.panelsByColour[numColour] = wx.Panel(self, \ + self.panelsByColour[numColour] = wx.Panel(self, \ pos=(posX, 0), size=(panelSizeW, panelSizeH)) - self.panelsByColour[numColour].SetBackgroundColour( \ + self.panelsByColour[numColour].SetBackgroundColour( \ wx.Colour(mircColours[numColour])) self.panelsByColour[numColour].Bind(wx.EVT_LEFT_DOWN, self.onLeftDown) self.panelsByColour[numColour].Bind(wx.EVT_RIGHT_DOWN, self.onRightDown) @@ -204,7 +297,7 @@ class MiRCARTFrame(wx.Frame): # }}} # {{{ onFileSaveAs(): XXX def onFileSaveAs(self, event): - with wx.FileDialog(self, "Save As...", os.getcwd(), "", \ + with wx.FileDialog(self, "Save As...", os.getcwd(), "", \ "*.txt", wx.FD_SAVE|wx.FD_OVERWRITE_PROMPT) as dialog: if dialog.ShowModal() == wx.ID_CANCEL: return @@ -220,7 +313,7 @@ class MiRCARTFrame(wx.Frame): canvasColBg = canvasMap[canvasRow][canvasCol][0] canvasColFg = canvasMap[canvasRow][canvasCol][1] canvasColText = canvasMap[canvasRow][canvasCol][2] - if colourLastBg != canvasColBg \ + if colourLastBg != canvasColBg \ or colourLastFg != canvasColFg: colourLastBg = canvasColBg; colourLastFg = canvasColFg; file.write("" + str(canvasColFg) + "," + str(canvasColBg)) @@ -249,9 +342,10 @@ class MiRCARTFrame(wx.Frame): self.menuBar.Append(self.menuFile, "&File") self.panelSkin = wx.Panel(self, wx.ID_ANY) - self.panelCanvas = MiRCARTCanvas(self.panelSkin, \ - canvasPos=canvasPos, cellSize=cellSize, canvasSize=canvasSize) - self.panelPalette = MiRCARTPalette(self.panelSkin, \ + self.panelCanvas = MiRCARTCanvas(self.panelSkin, \ + canvasPos=canvasPos, cellSize=cellSize, \ + canvasSize=canvasSize, canvasTools=[MiRCARTToolRect]) + self.panelPalette = MiRCARTPalette(self.panelSkin, \ (25, (canvasSize[1] + 3) * cellSize[1]), cellSize, self.onPaletteEvent) self.statusBar = self.CreateStatusBar() From e0300331fdf9c90419d7015e17618c9de62d864d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Thu, 4 Jan 2018 23:45:30 +0100 Subject: [PATCH 047/148] MiRC2png.py:MiRC2png._parseAs{Char,ColourSpec}(): fix indentation. --- MiRC2png.py | 94 ++++++++++++++++++++++++++--------------------------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/MiRC2png.py b/MiRC2png.py index adf3b44..abc18b1 100755 --- a/MiRC2png.py +++ b/MiRC2png.py @@ -112,58 +112,58 @@ class MiRC2png: # }}} # {{{ _parseAsChar(): Parse single character as regular character and mutate state def _parseAsChar(self, char): - if char == "": - self.inCurCol += 1; self.inCurBold = 0 if self.inCurBold else 1; - elif char == "": - self._State = self._State.STATE_COLOUR_SPEC; self.inCurCol += 1; - elif char == "": - self.inCurCol += 1; self.inCurItalic = 0 if self.inCurItalic else 1; - elif char == "": - self.inCurCol += 1; - self.inCurBold = 0; self.inCurItalic = 0; self.inCurUnderline = 0; - self.inCurColourSpec = ""; - elif char == "": - self.inCurCol += 1 - self.outCurColourBg, self.outCurColourFg = self.outCurColourFg, self.outCurColourBg; - elif char == "": - self.inCurCol += 1; self.inCurUnderline = 0 if self.inCurUnderline else 1; - elif char == " ": - if self.inCurBold: - colourBg = self._ColourMapBold[self.outCurColourBg] - else: - colourBg = self._ColourMapNormal[self.outCurColourBg] - self.outImgDraw.rectangle(((self.outCurX, self.outCurY), (self.outCurX + self.outImgFontSize[0], self.outCurY + self.outImgFontSize[1])), fill=colourBg) - if self.inCurUnderline: - self.outImgDraw.line((self.outCurX, self.outCurY + (self.outImgFontSize[1] - 2), self.outCurX + self.outImgFontSize[0], self.outCurY + (self.outImgFontSize[1] - 2)), fill=colourFg) - self.outCurX += self.outImgFontSize[0]; self.inCurCol += 1; + if char == "": + self.inCurCol += 1; self.inCurBold = 0 if self.inCurBold else 1; + elif char == "": + self._State = self._State.STATE_COLOUR_SPEC; self.inCurCol += 1; + elif char == "": + self.inCurCol += 1; self.inCurItalic = 0 if self.inCurItalic else 1; + elif char == "": + self.inCurCol += 1; + self.inCurBold = 0; self.inCurItalic = 0; self.inCurUnderline = 0; + self.inCurColourSpec = ""; + elif char == "": + self.inCurCol += 1 + self.outCurColourBg, self.outCurColourFg = self.outCurColourFg, self.outCurColourBg; + elif char == "": + self.inCurCol += 1; self.inCurUnderline = 0 if self.inCurUnderline else 1; + elif char == " ": + if self.inCurBold: + colourBg = self._ColourMapBold[self.outCurColourBg] else: - if self.inCurBold: - colourBg = self._ColourMapBold[self.outCurColourBg] - colourFg = self._ColourMapBold[self.outCurColourFg] - else: - colourBg = self._ColourMapNormal[self.outCurColourBg] - colourFg = self._ColourMapNormal[self.outCurColourFg] - self.outImgDraw.rectangle(((self.outCurX, self.outCurY), (self.outCurX + self.outImgFontSize[0], self.outCurY + self.outImgFontSize[1])), fill=colourBg) - # XXX implement italic - self.outImgDraw.text((self.outCurX, self.outCurY), char, colourFg, self.outImgFont) - if self.inCurUnderline: - self.outImgDraw.line((self.outCurX, self.outCurY + (self.outImgFontSize[1] - 2), self.outCurX + self.outImgFontSize[0], self.outCurY + (self.outImgFontSize[1] - 2)), fill=colourFg) - self.outCurX += self.outImgFontSize[0]; self.inCurCol += 1; + colourBg = self._ColourMapNormal[self.outCurColourBg] + self.outImgDraw.rectangle(((self.outCurX, self.outCurY), (self.outCurX + self.outImgFontSize[0], self.outCurY + self.outImgFontSize[1])), fill=colourBg) + if self.inCurUnderline: + self.outImgDraw.line((self.outCurX, self.outCurY + (self.outImgFontSize[1] - 2), self.outCurX + self.outImgFontSize[0], self.outCurY + (self.outImgFontSize[1] - 2)), fill=colourFg) + self.outCurX += self.outImgFontSize[0]; self.inCurCol += 1; + else: + if self.inCurBold: + colourBg = self._ColourMapBold[self.outCurColourBg] + colourFg = self._ColourMapBold[self.outCurColourFg] + else: + colourBg = self._ColourMapNormal[self.outCurColourBg] + colourFg = self._ColourMapNormal[self.outCurColourFg] + self.outImgDraw.rectangle(((self.outCurX, self.outCurY), (self.outCurX + self.outImgFontSize[0], self.outCurY + self.outImgFontSize[1])), fill=colourBg) + # XXX implement italic + self.outImgDraw.text((self.outCurX, self.outCurY), char, colourFg, self.outImgFont) + if self.inCurUnderline: + self.outImgDraw.line((self.outCurX, self.outCurY + (self.outImgFontSize[1] - 2), self.outCurX + self.outImgFontSize[0], self.outCurY + (self.outImgFontSize[1] - 2)), fill=colourFg) + self.outCurX += self.outImgFontSize[0]; self.inCurCol += 1; # }}} # {{{ _parseAsColourSpec(): Parse single character as mIRC colour control code sequence and mutate state def _parseAsColourSpec(self, char): - if char in set(",0123456789"): - self.inCurColourSpec += char; self.inCurCol += 1; + if char in set(",0123456789"): + self.inCurColourSpec += char; self.inCurCol += 1; + else: + self.inCurColourSpec = self.inCurColourSpec.split(",") + if len(self.inCurColourSpec) == 2: + self.outCurColourFg = int(self.inCurColourSpec[0]) + self.outCurColourBg = int(self.inCurColourSpec[1] or self.outCurColourBg) + elif len(self.inCurColourSpec) == 1: + self.outCurColourFg = int(self.inCurColourSpec[0]) else: - self.inCurColourSpec = self.inCurColourSpec.split(",") - if len(self.inCurColourSpec) == 2: - self.outCurColourFg = int(self.inCurColourSpec[0]) - self.outCurColourBg = int(self.inCurColourSpec[1] or self.outCurColourBg) - elif len(self.inCurColourSpec) == 1: - self.outCurColourFg = int(self.inCurColourSpec[0]) - else: - self.outCurColourBg = 1; self.outCurColourFg = 15; - self.inCurColourSpec = ""; self._State = self._State.STATE_CHAR; + self.outCurColourBg = 1; self.outCurColourFg = 15; + self.inCurColourSpec = ""; self._State = self._State.STATE_CHAR; # }}} # From 68ba42f2444872cde1da610b8670fcce458000a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Fri, 5 Jan 2018 00:52:02 +0100 Subject: [PATCH 048/148] MiRC2png.py: fix mIRC colour control code sequence counting & rendering logic. --- MiRC2png.py | 148 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 96 insertions(+), 52 deletions(-) diff --git a/MiRC2png.py b/MiRC2png.py index abc18b1..5ee3eee 100755 --- a/MiRC2png.py +++ b/MiRC2png.py @@ -39,7 +39,7 @@ class MiRC2png: inCurBold = inCurItalic = inCurUnderline = None; inCurColourSpec = None; state = None; - inCurCol = None; + inCurCol = inCurDigits = None; # {{{ _ColourMapBold: mIRC colour number to RGBA map given ^B (bold) _ColourMapBold = [ @@ -85,48 +85,104 @@ class MiRC2png: class _State(Enum): STATE_CHAR = 1 STATE_COLOUR_SPEC = 2 + STATE_CSPEC_DIGIT0 = 2 + STATE_CSPEC_DIGIT1 = 3 # }}} + # {{{ _countChar(): XXX + def _countChar(self, char): + return True + # }}} + # {{{ _countColourSpecState(): XXX + def _countColourSpecState(self, colourSpec): + return 0 + # }}} + # {{{ _render(): XXX + def _render(self): + self.outCurX = 0; self.outCurY = 0; + for inCurRow in range(0, len(self.inLines)): + self.inCurBold = 0; self.inCurItalic = 0; self.inCurUnderline = 0; + self.inCurColourSpec = ""; self._State = self._State.STATE_CHAR; + self.inCurCol = 0; self.inCurDigits = 0; + while self.inCurCol < len(self.inLines[inCurRow]): + if self._State == self._State.STATE_CHAR: + self._parseAsChar( \ + self.inLines[inCurRow][self.inCurCol], \ + self._syncChar) + elif self._State == self._State.STATE_CSPEC_DIGIT0 \ + or self._State == self._State.STATE_CSPEC_DIGIT1: \ + self._parseAsColourSpec( \ + self.inLines[inCurRow][self.inCurCol], \ + self._syncColourSpecState) + self.outCurX = 0; self.outCurY += self.outImgFontSize[1]; + # }}} # {{{ _getMaxCols(): Calculate widest row in lines, ignoring non-printable & mIRC control code sequences def _getMaxCols(self, lines): maxCols = 0; - for curRow in range(0, len(lines)): - curRowCols = 0; curState = self._State.STATE_CHAR; - curCol = 0; curColLen = len(lines[curRow]); - while curCol < curColLen: - curChar = lines[curRow][curCol] - if curState == self._State.STATE_CHAR: - if curChar == "": - curState = self._State.STATE_COLOUR_SPEC; curCol += 1; - elif curChar in string.printable: - curRowCols += 1; curCol += 1; - else: - curCol += 1; - elif curState == self._State.STATE_COLOUR_SPEC: - if curChar in set(",0123456789"): - curCol += 1; - else: - curState = self._State.STATE_CHAR; + for inCurRow in range(0, len(lines)): + self.inCurBold = 0; self.inCurItalic = 0; self.inCurUnderline = 0; + self.inCurColourSpec = ""; self._State = self._State.STATE_CHAR; + self.inCurCol = 0; self.inCurDigits = 0; curRowCols = 0; + while self.inCurCol < len(self.inLines[inCurRow]): + if self._State == self._State.STATE_CHAR: + if self._parseAsChar( \ + self.inLines[inCurRow][self.inCurCol], self._countChar): + curRowCols += 1 + elif self._State == self._State.STATE_CSPEC_DIGIT0 \ + or self._State == self._State.STATE_CSPEC_DIGIT1: + self._parseAsColourSpec( \ + self.inLines[inCurRow][self.inCurCol], \ + self._countColourSpecState) maxCols = max(maxCols, curRowCols) return maxCols # }}} # {{{ _parseAsChar(): Parse single character as regular character and mutate state - def _parseAsChar(self, char): + def _parseAsChar(self, char, fn): + if char == "": + self._State = self._State.STATE_CSPEC_DIGIT0; self.inCurCol += 1; + return False + else: + self.inCurCol += 1; return fn(char); + # }}} + # {{{ _parseAsColourSpec(): Parse single character as mIRC colour control code sequence and mutate state + def _parseAsColourSpec(self, char, fn): + if self._State == self._State.STATE_CSPEC_DIGIT0 \ + and char == ",": + self.inCurColourSpec += char; self.inCurCol += 1; + self._State = self._State.STATE_CSPEC_DIGIT1; + self.inCurDigits = 0 + return [False] + elif self._State == self._State.STATE_CSPEC_DIGIT0 \ + and char in set("0123456789") \ + and self.inCurDigits <= 1: + self.inCurColourSpec += char; self.inCurCol += 1; + self.inCurDigits += 1 + return [False] + elif self._State == self._State.STATE_CSPEC_DIGIT1 \ + and char in set("0123456789") \ + and self.inCurDigits <= 1: + self.inCurColourSpec += char; self.inCurCol += 1; + self.inCurDigits += 1 + return [False] + else: + result = fn(self.inCurColourSpec) + self.inCurColourSpec = ""; self._State = self._State.STATE_CHAR; + self.inCurDigits = 0 + return [True, result] + # }}} + # {{{ _syncChar(): XXX + def _syncChar(self, char): if char == "": - self.inCurCol += 1; self.inCurBold = 0 if self.inCurBold else 1; - elif char == "": - self._State = self._State.STATE_COLOUR_SPEC; self.inCurCol += 1; + self.inCurBold = 0 if self.inCurBold else 1; elif char == "": - self.inCurCol += 1; self.inCurItalic = 0 if self.inCurItalic else 1; + self.inCurItalic = 0 if self.inCurItalic else 1; elif char == "": - self.inCurCol += 1; self.inCurBold = 0; self.inCurItalic = 0; self.inCurUnderline = 0; self.inCurColourSpec = ""; elif char == "": - self.inCurCol += 1 self.outCurColourBg, self.outCurColourFg = self.outCurColourFg, self.outCurColourBg; elif char == "": - self.inCurCol += 1; self.inCurUnderline = 0 if self.inCurUnderline else 1; + self.inCurUnderline = 0 if self.inCurUnderline else 1; elif char == " ": if self.inCurBold: colourBg = self._ColourMapBold[self.outCurColourBg] @@ -135,7 +191,7 @@ class MiRC2png: self.outImgDraw.rectangle(((self.outCurX, self.outCurY), (self.outCurX + self.outImgFontSize[0], self.outCurY + self.outImgFontSize[1])), fill=colourBg) if self.inCurUnderline: self.outImgDraw.line((self.outCurX, self.outCurY + (self.outImgFontSize[1] - 2), self.outCurX + self.outImgFontSize[0], self.outCurY + (self.outImgFontSize[1] - 2)), fill=colourFg) - self.outCurX += self.outImgFontSize[0]; self.inCurCol += 1; + self.outCurX += self.outImgFontSize[0]; else: if self.inCurBold: colourBg = self._ColourMapBold[self.outCurColourBg] @@ -148,22 +204,20 @@ class MiRC2png: self.outImgDraw.text((self.outCurX, self.outCurY), char, colourFg, self.outImgFont) if self.inCurUnderline: self.outImgDraw.line((self.outCurX, self.outCurY + (self.outImgFontSize[1] - 2), self.outCurX + self.outImgFontSize[0], self.outCurY + (self.outImgFontSize[1] - 2)), fill=colourFg) - self.outCurX += self.outImgFontSize[0]; self.inCurCol += 1; + self.outCurX += self.outImgFontSize[0]; + return True # }}} - # {{{ _parseAsColourSpec(): Parse single character as mIRC colour control code sequence and mutate state - def _parseAsColourSpec(self, char): - if char in set(",0123456789"): - self.inCurColourSpec += char; self.inCurCol += 1; + # {{{ _syncColourSpecState(): XXX + def _syncColourSpecState(self, colourSpec): + colourSpec = colourSpec.split(",") + if len(colourSpec) == 2: + self.outCurColourFg = int(colourSpec[0]) + self.outCurColourBg = int(colourSpec[1] or self.outCurColourBg) + elif len(colourSpec) == 1: + self.outCurColourFg = int(colourSpec[0]) else: - self.inCurColourSpec = self.inCurColourSpec.split(",") - if len(self.inCurColourSpec) == 2: - self.outCurColourFg = int(self.inCurColourSpec[0]) - self.outCurColourBg = int(self.inCurColourSpec[1] or self.outCurColourBg) - elif len(self.inCurColourSpec) == 1: - self.outCurColourFg = int(self.inCurColourSpec[0]) - else: - self.outCurColourBg = 1; self.outCurColourFg = 15; - self.inCurColourSpec = ""; self._State = self._State.STATE_CHAR; + self.outCurColourBg = 1; self.outCurColourFg = 15; + return True # }}} # @@ -179,17 +233,7 @@ class MiRC2png: self.outImg = Image.new("RGBA", (self.inColsMax * self.outImgFontSize[0], self.inRows * self.outImgFontSize[1]), self._ColourMapNormal[1]) self.outImgDraw = ImageDraw.Draw(self.outImg) self.outCurColourBg = 1; self.outCurColourFg = 15; - self.outCurX = 0; self.outCurY = 0; - for inCurRow in range(0, len(self.inLines)): - self.inCurBold = 0; self.inCurItalic = 0; self.inCurUnderline = 0; - self.inCurColourSpec = ""; self._State = self._State.STATE_CHAR; - self.inCurCol = 0; - while self.inCurCol < len(self.inLines[inCurRow]): - if self._State == self._State.STATE_CHAR: - self._parseAsChar(self.inLines[inCurRow][self.inCurCol]) - elif self._State == self._State.STATE_COLOUR_SPEC: - self._parseAsColourSpec(self.inLines[inCurRow][self.inCurCol]) - self.outCurX = 0; self.outCurY += self.outImgFontSize[1]; + self._render() self.inFile.close(); self.outImg.save(imgFilePath); From 7a891d9863d5c0a9c48136d549ad6c5d25fcae77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Fri, 5 Jan 2018 00:57:22 +0100 Subject: [PATCH 049/148] MiRCART.py:MiRCARTToolRect.onMouse{Down,Motion}(): remove debugging print()s. --- MiRCART.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/MiRCART.py b/MiRCART.py index e0d7c91..f940fb3 100755 --- a/MiRCART.py +++ b/MiRCART.py @@ -233,14 +233,10 @@ class MiRCARTToolRect(MiRCARTTool): # }}} # {{{ onMouseDown(): XXX def onMouseDown(self, event, mapX, mapY, isLeftDown, isRightDown): - print("mouse down") return self._draw(event, mapX, mapY, isLeftDown, isRightDown) # }}} # {{{ onMouseMotion(): XXX def onMouseMotion(self, event, mapX, mapY, isLeftDown, isRightDown): - print("mouse moving") - if isLeftDown or isRightDown: - print("mouse depressed") return self._draw(event, mapX, mapY, isLeftDown, isRightDown) # }}} # {{{ Initialisation method From 423f923b4ede6fa5092375de52cb821582d26f49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Fri, 5 Jan 2018 01:34:57 +0100 Subject: [PATCH 050/148] MiRCART.py:MiRCARTCanvas._processMapPatches(): don't process patches that don't introduce changes.. MiRCART.py:MiRCARTCanvas._processMapPatches(): append redo and undo patches to self.patchesUndo. MiRCART.py:MiRCARTCanvas.undo(): initial implementation. MiRCART.py:MiRCARTFrame.{menuFileUndo,onFileUndo,__init__}(): add GUI for MiRCARTCanvas.undo(). --- MiRCART.py | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/MiRCART.py b/MiRCART.py index f940fb3..c3a1a70 100755 --- a/MiRCART.py +++ b/MiRCART.py @@ -105,9 +105,15 @@ class MiRCARTCanvas(wx.Panel): atY + patch[1], mapItem[0], mapItem[1], mapItem[2])) self._drawPatch(patch, eventDc, tmpDc, atX, atY) else: - self._drawPatch(patch, eventDc, tmpDc, atX, atY) - self.canvasMap[atY + patch[1]][atX + patch[0]] =\ - [patch[2], patch[3], " "]; + mapItem = self.canvasMap[atY + patch[1]][atX + patch[0]] + if mapItem != [patch[2], patch[3], patch[4]]: + self.patchesUndo.append((atX + patch[0], \ + atY + patch[1], patch[2], patch[3], " ")) + self.patchesUndo.append((atX + patch[0], \ + atY + patch[1], mapItem[0], mapItem[1], mapItem[2])) + self.canvasMap[atY + patch[1]][atX + patch[0]] =\ + [patch[2], patch[3], " "]; + self._drawPatch(patch, eventDc, tmpDc, atX, atY) # }}} # {{{ getBackgroundColour(): XXX def getBackgroundColour(self): @@ -160,6 +166,19 @@ class MiRCARTCanvas(wx.Panel): def onRightDown(self, event): self._onMouseEvent(event) # }}} + # {{{ undo(): XXX + def undo(self): + if len(self.patchesUndo) >= 2: + deltaPatch = self.patchesUndo[-1]; + del self.patchesUndo[-1]; del self.patchesUndo[-1]; + self.canvasMap[deltaPatch[1]][deltaPatch[0]] = \ + [deltaPatch[2], deltaPatch[3], deltaPatch[4]] + eventDc = wx.ClientDC(self); tmpDc = wx.MemoryDC(); + tmpDc.SelectObject(self.canvasBitmap) + self._drawPatch(deltaPatch, eventDc, tmpDc, 0, 0) + else: + return False + # }}} # {{{ Initialisation method def __init__(self, parent, canvasPos, cellSize, canvasSize, canvasTools): canvasWinSize = (cellSize[0] * canvasSize[0], cellSize[1] * canvasSize[1]) @@ -278,7 +297,7 @@ class MiRCARTPalette(wx.Panel): class MiRCARTFrame(wx.Frame): """XXX""" - menuFile = menuFileSaveAs = menuFileExit = menuBar = None + menuFile = menuFileUndo = menuFileSaveAs = menuFileExit = menuBar = None panelSkin = panelCanvas = panelPalette = None statusBar = None @@ -291,6 +310,10 @@ class MiRCARTFrame(wx.Frame): text += " " + str(self.panelCanvas.getBackgroundColour()) self.statusBar.SetStatusText(text) # }}} + # {{{ onFileUndo(): XXX + def onFileUndo(self, event): + self.panelCanvas.undo() + # }}} # {{{ onFileSaveAs(): XXX def onFileSaveAs(self, event): with wx.FileDialog(self, "Save As...", os.getcwd(), "", \ @@ -332,6 +355,7 @@ class MiRCARTFrame(wx.Frame): super().__init__(parent, wx.ID_ANY, "MiRCART", size=appSize) self.menuFile = wx.Menu() + self.menuFileUndo = self.menuFile.Append(wx.ID_SAVE, "&Undo", "Undo") self.menuFileSaveAs = self.menuFile.Append(wx.ID_SAVE, "Save &As...", "Save As...") self.menuFileExit = self.menuFile.Append(wx.ID_EXIT, "E&xit", "Exit") self.menuBar = wx.MenuBar() @@ -349,6 +373,7 @@ class MiRCARTFrame(wx.Frame): self.Bind(wx.EVT_MENU, self.onFileExit, self.menuFileExit) self.Bind(wx.EVT_MENU, self.onFileSaveAs, self.menuFileSaveAs) + self.Bind(wx.EVT_MENU, self.onFileUndo, self.menuFileUndo) self.SetMenuBar(self.menuBar) self.Show(True) # }}} From 8b6d7ab4454c56859dd7bdcfce893fb8d3f7e1d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Fri, 5 Jan 2018 01:59:04 +0100 Subject: [PATCH 051/148] MiRCART.py:MiRCARTFrame.accel{UndoId,Table}: added. MiRCART.py:MiRCARTFrame.onAccelUndo(): call self.panelCanvas.undo(). MiRCART.py:MiRCARTFrame.__init__(): create self.menuFileUndo w/ wx.ID_UNDO. MiRCART.py:MiRCARTFrame.__init__(): bind ^Z accelerator to self.onAccelUndo(). --- MiRCART.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/MiRCART.py b/MiRCART.py index c3a1a70..5b4ed7b 100755 --- a/MiRCART.py +++ b/MiRCART.py @@ -299,7 +299,7 @@ class MiRCARTFrame(wx.Frame): """XXX""" menuFile = menuFileUndo = menuFileSaveAs = menuFileExit = menuBar = None panelSkin = panelCanvas = panelPalette = None - statusBar = None + accelUndoId = accelTable = statusBar = None # {{{ _updateStatusBar(): XXX def _updateStatusBar(self): @@ -310,6 +310,10 @@ class MiRCARTFrame(wx.Frame): text += " " + str(self.panelCanvas.getBackgroundColour()) self.statusBar.SetStatusText(text) # }}} + # {{{ onAccelUndo(): XXX + def onAccelUndo(self, event): + self.panelCanvas.undo() + # }}} # {{{ onFileUndo(): XXX def onFileUndo(self, event): self.panelCanvas.undo() @@ -355,11 +359,12 @@ class MiRCARTFrame(wx.Frame): super().__init__(parent, wx.ID_ANY, "MiRCART", size=appSize) self.menuFile = wx.Menu() - self.menuFileUndo = self.menuFile.Append(wx.ID_SAVE, "&Undo", "Undo") + self.menuFileUndo = self.menuFile.Append(wx.ID_UNDO, "&Undo", "Undo") self.menuFileSaveAs = self.menuFile.Append(wx.ID_SAVE, "Save &As...", "Save As...") self.menuFileExit = self.menuFile.Append(wx.ID_EXIT, "E&xit", "Exit") self.menuBar = wx.MenuBar() self.menuBar.Append(self.menuFile, "&File") + self.SetMenuBar(self.menuBar) self.panelSkin = wx.Panel(self, wx.ID_ANY) self.panelCanvas = MiRCARTCanvas(self.panelSkin, \ @@ -368,13 +373,18 @@ class MiRCARTFrame(wx.Frame): self.panelPalette = MiRCARTPalette(self.panelSkin, \ (25, (canvasSize[1] + 3) * cellSize[1]), cellSize, self.onPaletteEvent) + self.accelUndoId = wx.NewId() + self.accelTable = wx.AcceleratorTable([( \ + wx.ACCEL_CTRL, ord('Z'), self.accelUndoId)]) + self.SetAcceleratorTable(self.accelTable) self.statusBar = self.CreateStatusBar() self._updateStatusBar() + self.SetFocus() + self.Bind(wx.EVT_MENU, self.onAccelUndo, id=self.accelUndoId) self.Bind(wx.EVT_MENU, self.onFileExit, self.menuFileExit) self.Bind(wx.EVT_MENU, self.onFileSaveAs, self.menuFileSaveAs) self.Bind(wx.EVT_MENU, self.onFileUndo, self.menuFileUndo) - self.SetMenuBar(self.menuBar) self.Show(True) # }}} From 496e4c7a4bd2efd0fb5896405166bf647f0d08ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Fri, 5 Jan 2018 03:27:57 +0100 Subject: [PATCH 052/148] MiRCART.py:MiRCARTToolRect.__processMapPatches(): always obtain cell state from canvasMap. MiRCART.py:MiRCARTToolRect._draw(): always paint w/ background colour given isRightDown. --- MiRCART.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/MiRCART.py b/MiRCART.py index 5b4ed7b..1b51b02 100755 --- a/MiRCART.py +++ b/MiRCART.py @@ -96,13 +96,16 @@ class MiRCARTCanvas(wx.Panel): mapPatchTmp = mapPatch[0]; mapPatchW = mapPatch[1]; mapPatchH = mapPatch[2]; if mapPatchTmp and self.patchesTmp: for patch in self.patchesTmp: + patch[2] = self.canvasMap[patch[1]][patch[0]][0] + patch[3] = self.canvasMap[patch[1]][patch[0]][1] + patch[4] = self.canvasMap[patch[1]][patch[0]][2] self._drawPatch(patch, eventDc, tmpDc, 0, 0) self.patchesTmp = [] for patch in mapPatch[3]: if mapPatchTmp: mapItem = self.canvasMap[atY + patch[1]][atX + patch[0]] - self.patchesTmp.append((atX + patch[0], \ - atY + patch[1], mapItem[0], mapItem[1], mapItem[2])) + self.patchesTmp.append([atX + patch[0], \ + atY + patch[1], None, None, None]) self._drawPatch(patch, eventDc, tmpDc, atX, atY) else: mapItem = self.canvasMap[atY + patch[1]][atX + patch[0]] @@ -243,8 +246,8 @@ class MiRCARTToolRect(MiRCARTTool): self.parentCanvas.getBackgroundColour(), \ self.parentCanvas.getBackgroundColour(), " "]]], \ [True, 1, 1, [[0, 0, \ - self.parentCanvas.getForegroundColour(), \ - self.parentCanvas.getForegroundColour(), " "]]]] + self.parentCanvas.getBackgroundColour(), \ + self.parentCanvas.getBackgroundColour(), " "]]]] else: return [[True, 1, 1, [[0, 0, \ self.parentCanvas.getForegroundColour(), \ From ba99dc1280cca38cf28cb2c91d6e83087d9a8783 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Fri, 5 Jan 2018 16:48:50 +0100 Subject: [PATCH 053/148] MiRC2png.py:MiRC2png._syncColourSpecState(): handle empty mIRC colour control code sequences. --- MiRC2png.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/MiRC2png.py b/MiRC2png.py index 5ee3eee..89c8976 100755 --- a/MiRC2png.py +++ b/MiRC2png.py @@ -209,14 +209,15 @@ class MiRC2png: # }}} # {{{ _syncColourSpecState(): XXX def _syncColourSpecState(self, colourSpec): - colourSpec = colourSpec.split(",") - if len(colourSpec) == 2: - self.outCurColourFg = int(colourSpec[0]) - self.outCurColourBg = int(colourSpec[1] or self.outCurColourBg) - elif len(colourSpec) == 1: - self.outCurColourFg = int(colourSpec[0]) - else: - self.outCurColourBg = 1; self.outCurColourFg = 15; + if len(colourSpec) > 0: + colourSpec = colourSpec.split(",") + if len(colourSpec) == 2: + self.outCurColourFg = int(colourSpec[0]) + self.outCurColourBg = int(colourSpec[1] or self.outCurColourBg) + elif len(colourSpec) == 1: + self.outCurColourFg = int(colourSpec[0]) + else: + self.outCurColourBg = 1; self.outCurColourFg = 15; return True # }}} From 31d644515a2974fc2a1a3848de6691c876067da4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Fri, 5 Jan 2018 16:59:51 +0100 Subject: [PATCH 054/148] README.md:MiRCART.py: add prerequisites for Windows & title. --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cfde2b4..4167833 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ -# MiRCART -- XXX -* Prerequisites: python3 && python-wx{gtk2.8,tools} on Debian-family Linux distributions +# MiRCART.py -- mIRC art editor for Windows & Linux +* Prerequisites on Windows: install Python v3.6.x[1] and wxPython v4.x.x w/ the following elevated command prompt command line: + `pip install wxPython` +* Prerequisites on Linux: python3 && python-wx{gtk2.8,tools} on Debian-family Linux distributions # IrcMiRCARTBot.py -- XXX * Prerequisites: python3 && python3-{json,requests,urllib3} on Debian-family Linux distributions @@ -8,3 +10,6 @@ # MiRC2png.py -- convert ASCII w/ mIRC control codes to monospaced PNG (for EFnet #MiRCART) * Prerequisites: python3 && python3-pil on Debian-family Linux distributions * MiRC2png.py usage: MiRC2png.py `` `` [``] [``] + +References: +Fri, 05 Jan 2018 17:01:47 +0100 [1] Python Releases for Windows | Python.org From bcf52d8dc5f9a57feab282b5a2a826063a759e7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Fri, 5 Jan 2018 17:05:58 +0100 Subject: [PATCH 055/148] IrcMiRCARTBot.py, README.md:IrcMiRCARTBot.py: update title. --- IrcMiRCARTBot.py | 4 ++-- README.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/IrcMiRCARTBot.py b/IrcMiRCARTBot.py index 3af1764..7932e3d 100755 --- a/IrcMiRCARTBot.py +++ b/IrcMiRCARTBot.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# IrcMiRCARTBot.py -- XXX +# IrcMiRCARTBot.py -- IRC<->MiRC2png bot (for EFnet #MiRCART) # Copyright (c) 2018 Lucio Andrés Illanes Albornoz # # Permission is hereby granted, free of charge, to any person obtaining a copy @@ -29,7 +29,7 @@ import IrcClient, MiRC2png import requests, urllib.request class IrcMiRCARTBot(IrcClient.IrcClient): - """IRC<->MiRCART bot""" + """IRC<->MiRC2png bot""" clientChannelLastMessage = clientChannelOps = clientChannel = None clientChannelRejoin = None diff --git a/README.md b/README.md index 4167833..acff52d 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,11 @@ `pip install wxPython` * Prerequisites on Linux: python3 && python-wx{gtk2.8,tools} on Debian-family Linux distributions -# IrcMiRCARTBot.py -- XXX +# IrcMiRCARTBot.py -- IRC<->MiRC2png bot (for EFnet #MiRCART) * Prerequisites: python3 && python3-{json,requests,urllib3} on Debian-family Linux distributions * IrcMiRCARTBot.py usage: IrcMiRCARTBot.py `` [``] [``] [``] [``] [``] -# MiRC2png.py -- convert ASCII w/ mIRC control codes to monospaced PNG (for EFnet #MiRCART) +# MiRC2png.py -- convert ASCII w/ mIRC control codes to monospaced PNG * Prerequisites: python3 && python3-pil on Debian-family Linux distributions * MiRC2png.py usage: MiRC2png.py `` `` [``] [``] From 133c4dc240d40366f6afe1b5ae64c7a843bc43ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Fri, 5 Jan 2018 17:21:14 +0100 Subject: [PATCH 056/148] MiRCART.py:MiRCARTCanvas.patchesUndoLevel: added to track {un,re}do queue movement in both directions. MiRCART.py:MiRCARTCanvas._processMapPatches(): remove {un,re}do queue items 0..patchesUndoLevel when updating queue. MiRCART.py:MiRCARTCanvas._processMapPatches(): prepend to queue instead of appending. MiRCART.py:MiRCARTCanvas._processMapPatches(): insert {un,re}do queue items as single (list) item each. MiRCART.py:MiRCARTCanvas.redo(): decrement patchesUndoLevel & apply redo patch, if patchesUndoLevel > 0. MiRCART.py:MiRCARTCanvas.undo(): apply undo patch & increment patchesUndoLevel, if patchesUndo[patchesUndoLevel] != None. MiRCART.py:MiRCARTFrame.{menuFileRedo,onFileRedo(),__init__()}: adds `&Redo' File menu item w/ wx.ID_REDO. MiRCART.py:MiRCARTFrame.{accelRedoId,onAccelRedo(),__init__()}: binds ^Y accelerator to MiRCARTCanvas.redo(). MiRCART.py: update title. --- MiRCART.py | 77 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 53 insertions(+), 24 deletions(-) diff --git a/MiRCART.py b/MiRCART.py index 1b51b02..f22e66b 100755 --- a/MiRCART.py +++ b/MiRCART.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# MiRCART.py -- XXX +# MiRCART.py -- mIRC art editor for Windows & Linux # Copyright (c) 2018 Lucio Andrés Illanes Albornoz # # Permission is hereby granted, free of charge, to any person obtaining a copy @@ -51,7 +51,7 @@ class MiRCARTCanvas(wx.Panel): canvasPos = canvasSize = canvasWinSize = cellPos = cellSize = None canvasBitmap = canvasMap = canvasTools = None mircBg = mircFg = mircBrushes = mircPens = None - patchesTmp = patchesUndo = None + patchesTmp = patchesUndo = patchesUndoLevel = None # {{{ _drawPatch(): XXX def _drawPatch(self, patch, eventDc, tmpDc, atX, atY): @@ -104,18 +104,18 @@ class MiRCARTCanvas(wx.Panel): for patch in mapPatch[3]: if mapPatchTmp: mapItem = self.canvasMap[atY + patch[1]][atX + patch[0]] - self.patchesTmp.append([atX + patch[0], \ - atY + patch[1], None, None, None]) + self.patchesTmp.append([atX + patch[0], atY + patch[1], None, None, None]) self._drawPatch(patch, eventDc, tmpDc, atX, atY) else: mapItem = self.canvasMap[atY + patch[1]][atX + patch[0]] if mapItem != [patch[2], patch[3], patch[4]]: - self.patchesUndo.append((atX + patch[0], \ - atY + patch[1], patch[2], patch[3], " ")) - self.patchesUndo.append((atX + patch[0], \ - atY + patch[1], mapItem[0], mapItem[1], mapItem[2])) - self.canvasMap[atY + patch[1]][atX + patch[0]] =\ - [patch[2], patch[3], " "]; + if self.patchesUndoLevel > 0: + del self.patchesUndo[0:self.patchesUndoLevel] + self.patchesUndoLevel = 0 + self.patchesUndo.insert(0, ( \ + (atX + patch[0], atY + patch[1], mapItem[0], mapItem[1], mapItem[2]), \ + (atX + patch[0], atY + patch[1], patch[2], patch[3], " "))) + self.canvasMap[atY + patch[1]][atX + patch[0]] = [patch[2], patch[3], " "]; self._drawPatch(patch, eventDc, tmpDc, atX, atY) # }}} # {{{ getBackgroundColour(): XXX @@ -169,16 +169,31 @@ class MiRCARTCanvas(wx.Panel): def onRightDown(self, event): self._onMouseEvent(event) # }}} - # {{{ undo(): XXX - def undo(self): - if len(self.patchesUndo) >= 2: - deltaPatch = self.patchesUndo[-1]; - del self.patchesUndo[-1]; del self.patchesUndo[-1]; - self.canvasMap[deltaPatch[1]][deltaPatch[0]] = \ - [deltaPatch[2], deltaPatch[3], deltaPatch[4]] + # {{{ redo(): XXX + def redo(self): + if self.patchesUndoLevel > 0: + self.patchesUndoLevel -= 1 + redoPatch = self.patchesUndo[self.patchesUndoLevel][1] + self.canvasMap[redoPatch[1]][redoPatch[0]] = \ + [redoPatch[2], redoPatch[3], redoPatch[4]] eventDc = wx.ClientDC(self); tmpDc = wx.MemoryDC(); tmpDc.SelectObject(self.canvasBitmap) - self._drawPatch(deltaPatch, eventDc, tmpDc, 0, 0) + self._drawPatch(redoPatch, eventDc, tmpDc, 0, 0) + return True + else: + return False + # }}} + # {{{ undo(): XXX + def undo(self): + if self.patchesUndo[self.patchesUndoLevel] != None: + undoPatch = self.patchesUndo[self.patchesUndoLevel][0] + self.canvasMap[undoPatch[1]][undoPatch[0]] = \ + [undoPatch[2], undoPatch[3], undoPatch[4]] + eventDc = wx.ClientDC(self); tmpDc = wx.MemoryDC(); + tmpDc.SelectObject(self.canvasBitmap) + self._drawPatch(undoPatch, eventDc, tmpDc, 0, 0) + self.patchesUndoLevel += 1 + return True else: return False # }}} @@ -204,7 +219,8 @@ class MiRCARTCanvas(wx.Panel): self.mircPens[mircColour] = wx.Pen( \ wx.Colour(mircColours[mircColour]), 1) - self.patchesTmp = []; self.patchesUndo = []; + self.patchesTmp = [] + self.patchesUndo = [None]; self.patchesUndoLevel = 0; self.Bind(wx.EVT_LEFT_DOWN, self.onLeftDown) self.Bind(wx.EVT_MOTION, self.onMotion) @@ -300,9 +316,9 @@ class MiRCARTPalette(wx.Panel): class MiRCARTFrame(wx.Frame): """XXX""" - menuFile = menuFileUndo = menuFileSaveAs = menuFileExit = menuBar = None + menuFile = menuFileRedo = menuFileUndo = menuFileSaveAs = menuFileExit = menuBar = None panelSkin = panelCanvas = panelPalette = None - accelUndoId = accelTable = statusBar = None + accelRedoId = accelUndoId = accelTable = statusBar = None # {{{ _updateStatusBar(): XXX def _updateStatusBar(self): @@ -313,10 +329,18 @@ class MiRCARTFrame(wx.Frame): text += " " + str(self.panelCanvas.getBackgroundColour()) self.statusBar.SetStatusText(text) # }}} + # {{{ onAccelRedo(): XXX + def onAccelRedo(self, event): + self.panelCanvas.redo() + # }}} # {{{ onAccelUndo(): XXX def onAccelUndo(self, event): self.panelCanvas.undo() # }}} + # {{{ onFileRedo(): XXX + def onFileRedo(self, event): + self.panelCanvas.redo() + # }}} # {{{ onFileUndo(): XXX def onFileUndo(self, event): self.panelCanvas.undo() @@ -362,6 +386,7 @@ class MiRCARTFrame(wx.Frame): super().__init__(parent, wx.ID_ANY, "MiRCART", size=appSize) self.menuFile = wx.Menu() + self.menuFileRedo = self.menuFile.Append(wx.ID_REDO, "&Redo", "Redo") self.menuFileUndo = self.menuFile.Append(wx.ID_UNDO, "&Undo", "Undo") self.menuFileSaveAs = self.menuFile.Append(wx.ID_SAVE, "Save &As...", "Save As...") self.menuFileExit = self.menuFile.Append(wx.ID_EXIT, "E&xit", "Exit") @@ -376,17 +401,21 @@ class MiRCARTFrame(wx.Frame): self.panelPalette = MiRCARTPalette(self.panelSkin, \ (25, (canvasSize[1] + 3) * cellSize[1]), cellSize, self.onPaletteEvent) - self.accelUndoId = wx.NewId() - self.accelTable = wx.AcceleratorTable([( \ - wx.ACCEL_CTRL, ord('Z'), self.accelUndoId)]) + self.accelRedoId = wx.NewId(); self.accelUndoId = wx.NewId(); + accelTableEntries = [wx.AcceleratorEntry() for n in range(2)] + accelTableEntries[0].Set(wx.ACCEL_CTRL, ord('Y'), self.accelRedoId) + accelTableEntries[1].Set(wx.ACCEL_CTRL, ord('Z'), self.accelUndoId) + self.accelTable = wx.AcceleratorTable(accelTableEntries) self.SetAcceleratorTable(self.accelTable) self.statusBar = self.CreateStatusBar() self._updateStatusBar() self.SetFocus() + self.Bind(wx.EVT_MENU, self.onAccelRedo, id=self.accelRedoId) self.Bind(wx.EVT_MENU, self.onAccelUndo, id=self.accelUndoId) self.Bind(wx.EVT_MENU, self.onFileExit, self.menuFileExit) self.Bind(wx.EVT_MENU, self.onFileSaveAs, self.menuFileSaveAs) + self.Bind(wx.EVT_MENU, self.onFileRedo, self.menuFileRedo) self.Bind(wx.EVT_MENU, self.onFileUndo, self.menuFileUndo) self.Show(True) # }}} From 8215229f84aff34ed6aefc8fbdb556c9740c2f49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Fri, 5 Jan 2018 18:08:10 +0100 Subject: [PATCH 057/148] MiRCART.py:MiRCARTFrame: complete GUI skeleton. --- MiRCART.py | 162 ++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 137 insertions(+), 25 deletions(-) diff --git a/MiRCART.py b/MiRCART.py index f22e66b..a80d4bf 100755 --- a/MiRCART.py +++ b/MiRCART.py @@ -316,7 +316,16 @@ class MiRCARTPalette(wx.Panel): class MiRCARTFrame(wx.Frame): """XXX""" - menuFile = menuFileRedo = menuFileUndo = menuFileSaveAs = menuFileExit = menuBar = None + menuFile = None + menuFileNew = menuFileOpen = menuFileSave = menuFileSaveAs = None + menuFileExportPastebin = menuFileExportPng = None + menuFileExit = None + menuEdit = None + menuEditRedo = menuEditUndo = None + menuEditCopy = menuEditCut = menuEditDelete = menuEditPaste = None + menuEditDecrBrush = menuEditIncrBrush = menuEditSolidBrush = None + menuTools = menuToolsCircle = menuToolsLine = menuToolsRect = None + menuBar = None panelSkin = panelCanvas = panelPalette = None accelRedoId = accelUndoId = accelTable = statusBar = None @@ -337,14 +346,66 @@ class MiRCARTFrame(wx.Frame): def onAccelUndo(self, event): self.panelCanvas.undo() # }}} - # {{{ onFileRedo(): XXX - def onFileRedo(self, event): + # {{{ onEditCopy(): XXX + def onEditCopy(self, event): + pass + # }}} + # {{{ onEditCut(): XXX + def onEditCut(self, event): + pass + # }}} + # {{{ onEditDecrBrush(): XXX + def onEditDecrBrush(self, event): + pass + # }}} + # {{{ onEditDelete(): XXX + def onEditDelete(self, event): + pass + # }}} + # {{{ onEditIncrBrush(): XXX + def onEditIncrBrush(self, event): + pass + # }}} + # {{{ onEditPaste(): XXX + def onEditPaste(self, event): + pass + # }}} + # {{{ onEditRedo(): XXX + def onEditRedo(self, event): self.panelCanvas.redo() # }}} - # {{{ onFileUndo(): XXX - def onFileUndo(self, event): + # {{{ onEditSolidBrush(): XXX + def onEditSolidBrush(self, event): + pass + # }}} + # {{{ onEditUndo(): XXX + def onEditUndo(self, event): self.panelCanvas.undo() # }}} + # {{{ onFileExit(): XXX + def onFileExit(self, event): + self.Close(True) + # }}} + # {{{ onFileExportPastebin(): XXX + def onFileExportPastebin(self, event): + pass + # }}} + # {{{ onFileExportPng(): XXX + def onFileExportPng(self, event): + pass + # }}} + # {{{ onFileNew(): XXX + def onFileNew(self, event): + pass + # }}} + # {{{ onFileOpen(): XXX + def onFileOpen(self, event): + pass + # }}} + # {{{ onFileSave(): XXX + def onFileSave(self, event): + pass + # }}} # {{{ onFileSaveAs(): XXX def onFileSaveAs(self, event): with wx.FileDialog(self, "Save As...", os.getcwd(), "", \ @@ -372,9 +433,17 @@ class MiRCARTFrame(wx.Frame): except IOError as error: wx.LogError("IOError {}".format(error)) # }}} - # {{{ onFileExit(): XXX - def onFileExit(self, event): - self.Close(True) + # {{{ onToolsRect(): XXX + def onToolsRect(self, event): + pass + # }}} + # {{{ onToolsCircle(): XXX + def onToolsCircle(self, event): + pass + # }}} + # {{{ onToolsLine(): XXX + def onToolsLine(self, event): + pass # }}} # {{{ onPaletteEvent(): XXX def onPaletteEvent(self, leftDown, rightDown, numColour): @@ -385,15 +454,6 @@ class MiRCARTFrame(wx.Frame): def __init__(self, parent, appSize=(1024, 768), canvasPos=(25, 25), cellSize=(7, 14), canvasSize=(80, 25)): super().__init__(parent, wx.ID_ANY, "MiRCART", size=appSize) - self.menuFile = wx.Menu() - self.menuFileRedo = self.menuFile.Append(wx.ID_REDO, "&Redo", "Redo") - self.menuFileUndo = self.menuFile.Append(wx.ID_UNDO, "&Undo", "Undo") - self.menuFileSaveAs = self.menuFile.Append(wx.ID_SAVE, "Save &As...", "Save As...") - self.menuFileExit = self.menuFile.Append(wx.ID_EXIT, "E&xit", "Exit") - self.menuBar = wx.MenuBar() - self.menuBar.Append(self.menuFile, "&File") - self.SetMenuBar(self.menuBar) - self.panelSkin = wx.Panel(self, wx.ID_ANY) self.panelCanvas = MiRCARTCanvas(self.panelSkin, \ canvasPos=canvasPos, cellSize=cellSize, \ @@ -401,22 +461,74 @@ class MiRCARTFrame(wx.Frame): self.panelPalette = MiRCARTPalette(self.panelSkin, \ (25, (canvasSize[1] + 3) * cellSize[1]), cellSize, self.onPaletteEvent) - self.accelRedoId = wx.NewId(); self.accelUndoId = wx.NewId(); + self.menuFile = wx.Menu() + self.menuFileNew = self.menuFile.Append(wx.ID_NEW, "&New", "New") + self.Bind(wx.EVT_MENU, self.onFileNew, self.menuFileNew) + self.menuFileOpen = self.menuFile.Append(wx.ID_OPEN, "&Open...", "Open...") + self.Bind(wx.EVT_MENU, self.onFileOpen, self.menuFileOpen) + self.menuFileSave = self.menuFile.Append(wx.ID_SAVE, "&Save", "Save") + self.Bind(wx.EVT_MENU, self.onFileSave, self.menuFileSave) + self.menuFileSaveAs = self.menuFile.Append(wx.ID_SAVEAS, "Save &As...", "Save As...") + self.Bind(wx.EVT_MENU, self.onFileSaveAs, self.menuFileSaveAs) + self.menuFile.AppendSeparator() + self.menuFileExportPastebin = self.menuFile.Append(wx.NewId(), "Export to &Pastebin...", "Export to Pastebin...") + self.Bind(wx.EVT_MENU, self.onFileExportPastebin, self.menuFileExportPastebin) + self.menuFileExportPng = self.menuFile.Append(wx.NewId(), "Export as &PNG...", "Export as PNG...") + self.Bind(wx.EVT_MENU, self.onFileExportPng, self.menuFileExportPng) + self.menuFile.AppendSeparator() + self.menuFileExit = self.menuFile.Append(wx.ID_EXIT, "E&xit", "Exit") + self.Bind(wx.EVT_MENU, self.onFileExit, self.menuFileExit) + + self.menuEdit = wx.Menu() + self.menuEditUndo = self.menuEdit.Append(wx.ID_UNDO, "&Undo", "Undo") + self.Bind(wx.EVT_MENU, self.onEditUndo, self.menuEditUndo) + self.menuEditRedo = self.menuEdit.Append(wx.ID_REDO, "&Redo", "Redo") + self.Bind(wx.EVT_MENU, self.onEditRedo, self.menuEditRedo) + self.menuEdit.AppendSeparator() + self.menuEditCut = self.menuEdit.Append(wx.ID_CUT, "Cu&t", "Cut") + self.Bind(wx.EVT_MENU, self.onEditCut, self.menuEditCut) + self.menuEditCopy = self.menuEdit.Append(wx.ID_COPY, "&Copy", "Copy") + self.Bind(wx.EVT_MENU, self.onEditCopy, self.menuEditCopy) + self.menuEditPaste = self.menuEdit.Append(wx.ID_PASTE, "&Paste", "Paste") + self.Bind(wx.EVT_MENU, self.onEditPaste, self.menuEditPaste) + self.menuEditDelete = self.menuEdit.Append(wx.ID_DELETE, "De&lete", "Delete") + self.Bind(wx.EVT_MENU, self.onEditDelete, self.menuEditDelete) + self.menuEdit.AppendSeparator() + self.menuEditIncrBrush = self.menuEdit.Append(wx.NewId(), "&Increase brush size", "Increase brush size") + self.Bind(wx.EVT_MENU, self.onEditIncrBrush, self.menuEditIncrBrush) + self.menuEditDecrBrush = self.menuEdit.Append(wx.NewId(), "&Decrease brush size", "Decrease brush size") + self.Bind(wx.EVT_MENU, self.onEditDecrBrush, self.menuEditDecrBrush) + self.menuEditSolidBrush = self.menuEdit.AppendRadioItem(wx.NewId(), "&Solid brush", "Solid brush") + self.Bind(wx.EVT_MENU, self.onEditSolidBrush, self.menuEditSolidBrush) + + self.menuTools = wx.Menu() + self.menuToolsRect = self.menuTools.AppendRadioItem(wx.NewId(), "&Rectangle", "Rectangle") + self.Bind(wx.EVT_MENU, self.onToolsRect, self.menuToolsRect) + self.menuToolsCircle = self.menuTools.AppendRadioItem(wx.NewId(), "&Circle", "Circle") + self.Bind(wx.EVT_MENU, self.onToolsCircle, self.menuToolsCircle) + self.menuToolsLine = self.menuTools.AppendRadioItem(wx.NewId(), "&Line", "Line") + self.Bind(wx.EVT_MENU, self.onToolsLine, self.menuToolsLine) + + self.menuBar = wx.MenuBar() + self.menuBar.Append(self.menuFile, "&File") + self.menuBar.Append(self.menuEdit, "&Edit") + self.menuBar.Append(self.menuTools, "&Tools") + self.SetMenuBar(self.menuBar) + accelTableEntries = [wx.AcceleratorEntry() for n in range(2)] + self.accelRedoId = wx.NewId() accelTableEntries[0].Set(wx.ACCEL_CTRL, ord('Y'), self.accelRedoId) + self.Bind(wx.EVT_MENU, self.onAccelRedo, id=self.accelRedoId) + self.accelUndoId = wx.NewId() accelTableEntries[1].Set(wx.ACCEL_CTRL, ord('Z'), self.accelUndoId) + self.Bind(wx.EVT_MENU, self.onAccelUndo, id=self.accelUndoId) self.accelTable = wx.AcceleratorTable(accelTableEntries) self.SetAcceleratorTable(self.accelTable) + self.statusBar = self.CreateStatusBar() self._updateStatusBar() - self.SetFocus() - self.Bind(wx.EVT_MENU, self.onAccelRedo, id=self.accelRedoId) - self.Bind(wx.EVT_MENU, self.onAccelUndo, id=self.accelUndoId) - self.Bind(wx.EVT_MENU, self.onFileExit, self.menuFileExit) - self.Bind(wx.EVT_MENU, self.onFileSaveAs, self.menuFileSaveAs) - self.Bind(wx.EVT_MENU, self.onFileRedo, self.menuFileRedo) - self.Bind(wx.EVT_MENU, self.onFileUndo, self.menuFileUndo) + self.SetFocus() self.Show(True) # }}} From c7fbe5b20c69b3f1e79d5bc4e210a0c3bbaeb9fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Fri, 5 Jan 2018 18:09:25 +0100 Subject: [PATCH 058/148] MiRCART.py:MiRCARTFrame.__init__(): change default appSize to (800, 600). --- MiRCART.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiRCART.py b/MiRCART.py index a80d4bf..8c7a86b 100755 --- a/MiRCART.py +++ b/MiRCART.py @@ -451,7 +451,7 @@ class MiRCARTFrame(wx.Frame): self._updateStatusBar() # }}} # {{{ Initialisation method - def __init__(self, parent, appSize=(1024, 768), canvasPos=(25, 25), cellSize=(7, 14), canvasSize=(80, 25)): + def __init__(self, parent, appSize=(800, 600), canvasPos=(25, 25), cellSize=(7, 14), canvasSize=(80, 25)): super().__init__(parent, wx.ID_ANY, "MiRCART", size=appSize) self.panelSkin = wx.Panel(self, wx.ID_ANY) From 813d49a3f242c7a94ae37e8dd26c5355d76d0652 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Fri, 5 Jan 2018 18:12:02 +0100 Subject: [PATCH 059/148] MiRCART.py:MiRCARTFrame.__init__(): disable (grey out) menuEdit{Redo,Undo} initially. --- MiRCART.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MiRCART.py b/MiRCART.py index 8c7a86b..8d79396 100755 --- a/MiRCART.py +++ b/MiRCART.py @@ -481,8 +481,10 @@ class MiRCARTFrame(wx.Frame): self.menuEdit = wx.Menu() self.menuEditUndo = self.menuEdit.Append(wx.ID_UNDO, "&Undo", "Undo") + self.menuEditUndo.Enable(False) self.Bind(wx.EVT_MENU, self.onEditUndo, self.menuEditUndo) self.menuEditRedo = self.menuEdit.Append(wx.ID_REDO, "&Redo", "Redo") + self.menuEditRedo.Enable(False) self.Bind(wx.EVT_MENU, self.onEditRedo, self.menuEditRedo) self.menuEdit.AppendSeparator() self.menuEditCut = self.menuEdit.Append(wx.ID_CUT, "Cu&t", "Cut") From 28ae8f04f819ba2993a536ffa0d4b4431911e4e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Fri, 5 Jan 2018 18:21:38 +0100 Subject: [PATCH 060/148] MiRCART.py:MiRCARTCanvas.{parentFrame,__init__()}: added for MiRCARTFrame.onCanvasUpdate(). MiRCART.py:MiRCARTCanvas._processMapPatches(): call parentFrame.onCanvasUpdate() given updates. MiRCART.py:MiRCARTCanvas.{re,un}do(): call parentFrame.onCanvasUpdate() given {re,un}do. MiRCART.py:MiRCARTFrame.onCanvasUpdate(): {dis,en}able menuEdit{Re,Un}do on updates. --- MiRCART.py | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/MiRCART.py b/MiRCART.py index 8d79396..79b0354 100755 --- a/MiRCART.py +++ b/MiRCART.py @@ -48,6 +48,7 @@ mircColours = [ class MiRCARTCanvas(wx.Panel): """XXX""" + parentFrame = None canvasPos = canvasSize = canvasWinSize = cellPos = cellSize = None canvasBitmap = canvasMap = canvasTools = None mircBg = mircFg = mircBrushes = mircPens = None @@ -117,6 +118,8 @@ class MiRCARTCanvas(wx.Panel): (atX + patch[0], atY + patch[1], patch[2], patch[3], " "))) self.canvasMap[atY + patch[1]][atX + patch[0]] = [patch[2], patch[3], " "]; self._drawPatch(patch, eventDc, tmpDc, atX, atY) + if len(mapPatch[3]): + self.parentFrame.onCanvasUpdate() # }}} # {{{ getBackgroundColour(): XXX def getBackgroundColour(self): @@ -179,6 +182,7 @@ class MiRCARTCanvas(wx.Panel): eventDc = wx.ClientDC(self); tmpDc = wx.MemoryDC(); tmpDc.SelectObject(self.canvasBitmap) self._drawPatch(redoPatch, eventDc, tmpDc, 0, 0) + self.parentFrame.onCanvasUpdate() return True else: return False @@ -193,12 +197,14 @@ class MiRCARTCanvas(wx.Panel): tmpDc.SelectObject(self.canvasBitmap) self._drawPatch(undoPatch, eventDc, tmpDc, 0, 0) self.patchesUndoLevel += 1 + self.parentFrame.onCanvasUpdate() return True else: return False # }}} # {{{ Initialisation method - def __init__(self, parent, canvasPos, cellSize, canvasSize, canvasTools): + def __init__(self, parent, parentFrame, canvasPos, cellSize, canvasSize, canvasTools): + self.parentFrame = parentFrame canvasWinSize = (cellSize[0] * canvasSize[0], cellSize[1] * canvasSize[1]) super().__init__(parent, pos=canvasPos, size=canvasWinSize) self.canvasPos = canvasPos; self.canvasSize = canvasSize; self.canvasWinSize = canvasWinSize; @@ -346,6 +352,17 @@ class MiRCARTFrame(wx.Frame): def onAccelUndo(self, event): self.panelCanvas.undo() # }}} + # {{{ onCanvasUpdate(): XXX + def onCanvasUpdate(self): + if self.panelCanvas.patchesUndo[self.panelCanvas.patchesUndoLevel] != None: + self.menuEditUndo.Enable(True) + else: + self.menuEditUndo.Enable(False) + if self.panelCanvas.patchesUndoLevel > 0: + self.menuEditRedo.Enable(True) + else: + self.menuEditRedo.Enable(False) + # }}} # {{{ onEditCopy(): XXX def onEditCopy(self, event): pass @@ -433,6 +450,11 @@ class MiRCARTFrame(wx.Frame): except IOError as error: wx.LogError("IOError {}".format(error)) # }}} + # {{{ onPaletteEvent(): XXX + def onPaletteEvent(self, leftDown, rightDown, numColour): + self.panelCanvas.onPaletteEvent(leftDown, rightDown, numColour) + self._updateStatusBar() + # }}} # {{{ onToolsRect(): XXX def onToolsRect(self, event): pass @@ -445,20 +467,15 @@ class MiRCARTFrame(wx.Frame): def onToolsLine(self, event): pass # }}} - # {{{ onPaletteEvent(): XXX - def onPaletteEvent(self, leftDown, rightDown, numColour): - self.panelCanvas.onPaletteEvent(leftDown, rightDown, numColour) - self._updateStatusBar() - # }}} # {{{ Initialisation method def __init__(self, parent, appSize=(800, 600), canvasPos=(25, 25), cellSize=(7, 14), canvasSize=(80, 25)): super().__init__(parent, wx.ID_ANY, "MiRCART", size=appSize) self.panelSkin = wx.Panel(self, wx.ID_ANY) - self.panelCanvas = MiRCARTCanvas(self.panelSkin, \ - canvasPos=canvasPos, cellSize=cellSize, \ + self.panelCanvas = MiRCARTCanvas(self.panelSkin, \ + parentFrame=self, canvasPos=canvasPos, cellSize=cellSize, \ canvasSize=canvasSize, canvasTools=[MiRCARTToolRect]) - self.panelPalette = MiRCARTPalette(self.panelSkin, \ + self.panelPalette = MiRCARTPalette(self.panelSkin, \ (25, (canvasSize[1] + 3) * cellSize[1]), cellSize, self.onPaletteEvent) self.menuFile = wx.Menu() From 55381cf57b8dba0fd614180f5a1ac5f8bc1ff526 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Fri, 5 Jan 2018 18:27:01 +0100 Subject: [PATCH 061/148] MiRCART.py:MiRCARTFrame.__init__(): update File menu titles to "Export to Pasteb&in..." and "Export as PN&G...". --- MiRCART.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MiRCART.py b/MiRCART.py index 79b0354..23a3cb9 100755 --- a/MiRCART.py +++ b/MiRCART.py @@ -488,9 +488,9 @@ class MiRCARTFrame(wx.Frame): self.menuFileSaveAs = self.menuFile.Append(wx.ID_SAVEAS, "Save &As...", "Save As...") self.Bind(wx.EVT_MENU, self.onFileSaveAs, self.menuFileSaveAs) self.menuFile.AppendSeparator() - self.menuFileExportPastebin = self.menuFile.Append(wx.NewId(), "Export to &Pastebin...", "Export to Pastebin...") + self.menuFileExportPastebin = self.menuFile.Append(wx.NewId(), "Export to Pasteb&in...", "Export to Pastebin...") self.Bind(wx.EVT_MENU, self.onFileExportPastebin, self.menuFileExportPastebin) - self.menuFileExportPng = self.menuFile.Append(wx.NewId(), "Export as &PNG...", "Export as PNG...") + self.menuFileExportPng = self.menuFile.Append(wx.NewId(), "Export as PN&G...", "Export as PNG...") self.Bind(wx.EVT_MENU, self.onFileExportPng, self.menuFileExportPng) self.menuFile.AppendSeparator() self.menuFileExit = self.menuFile.Append(wx.ID_EXIT, "E&xit", "Exit") From 703aa2c388c4a063da9a3993087996bddf1bef9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Fri, 5 Jan 2018 20:45:21 +0100 Subject: [PATCH 062/148] MiRCART.py:MiRCARTPalette, MiRCARTFrame.panelPalette: replaced w/ toolbar items. MiRCART.py:MiRCARTFrame.toolBar*: added. MiRCART.py:MiRCARTFrame.onToolColour{Bg,Fg}(): hand off to onPaletteEvent(). MiRCART.py:MiRCARTFrame.__init__(): create & initialise toolbar. --- MiRCART.py | 134 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 96 insertions(+), 38 deletions(-) diff --git a/MiRCART.py b/MiRCART.py index 23a3cb9..fe2ef01 100755 --- a/MiRCART.py +++ b/MiRCART.py @@ -288,38 +288,6 @@ class MiRCARTToolRect(MiRCARTTool): super().__init__(parentCanvas) # }}} -class MiRCARTPalette(wx.Panel): - """XXX""" - panelsByColour = onPaletteEvent = None - - # {{{ onLeftDown(): XXX - def onLeftDown(self, event): - numColour = int(event.GetEventObject().GetName()) - self.onPaletteEvent(True, False, numColour) - # }}} - # {{{ onRightDown(): XXX - def onRightDown(self, event): - numColour = int(event.GetEventObject().GetName()) - self.onPaletteEvent(False, True, numColour) - # }}} - # {{{ Initialisation method - def __init__(self, parent, parentPos, cellSize, onPaletteEvent): - panelSizeW = 6 * cellSize[0]; panelSizeH = 2 * cellSize[1]; - paletteSize = (panelSizeW * 16, panelSizeH) - super().__init__(parent, pos=parentPos, size=paletteSize) - self.panelsByColour = [None] * len(mircColours) - for numColour in range(0, len(mircColours)): - posX = (numColour * (cellSize[0] * 6)) - self.panelsByColour[numColour] = wx.Panel(self, \ - pos=(posX, 0), size=(panelSizeW, panelSizeH)) - self.panelsByColour[numColour].SetBackgroundColour( \ - wx.Colour(mircColours[numColour])) - self.panelsByColour[numColour].Bind(wx.EVT_LEFT_DOWN, self.onLeftDown) - self.panelsByColour[numColour].Bind(wx.EVT_RIGHT_DOWN, self.onRightDown) - self.panelsByColour[numColour].SetName(str(numColour)) - self.onPaletteEvent = onPaletteEvent - # }}} - class MiRCARTFrame(wx.Frame): """XXX""" menuFile = None @@ -332,7 +300,14 @@ class MiRCARTFrame(wx.Frame): menuEditDecrBrush = menuEditIncrBrush = menuEditSolidBrush = None menuTools = menuToolsCircle = menuToolsLine = menuToolsRect = None menuBar = None - panelSkin = panelCanvas = panelPalette = None + panelSkin = panelCanvas = None + toolBar = None + toolBarIdNew = toolBarIdOpen = toolBarIdSave = toolBarIdSaveAs = None + toolBarIdUndo = toolBarIdRedo = None + toolBarIdCut = toolBarIdCopy = toolBarIdPaste = toolBarIdDelete = None + toolBarIdIncrBrush = toolBarIdDecrBrush = toolBarIdSolidBrush = None + toolBarIdRect = toolBarIdCircle = toolBarIdLine = None + toolBarIdColours = toolBarBitmapColours = None accelRedoId = accelUndoId = accelTable = statusBar = None # {{{ _updateStatusBar(): XXX @@ -455,6 +430,20 @@ class MiRCARTFrame(wx.Frame): self.panelCanvas.onPaletteEvent(leftDown, rightDown, numColour) self._updateStatusBar() # }}} + # {{{ onToolColourBg(): XXX + def onToolColourBg(self, event): + itemId = event.GetId() + for numColour in range(0, len(mircColours)): + if self.toolBarIdColours[numColour] == itemId: + self.onPaletteEvent(False, True, numColour) + # }}} + # {{{ onToolColourFg(): XXX + def onToolColourFg(self, event): + itemId = event.GetId() + for numColour in range(0, len(mircColours)): + if self.toolBarIdColours[numColour] == itemId: + self.onPaletteEvent(True, False, numColour) + # }}} # {{{ onToolsRect(): XXX def onToolsRect(self, event): pass @@ -468,15 +457,13 @@ class MiRCARTFrame(wx.Frame): pass # }}} # {{{ Initialisation method - def __init__(self, parent, appSize=(800, 600), canvasPos=(25, 25), cellSize=(7, 14), canvasSize=(80, 25)): + def __init__(self, parent, appSize=(800, 600), canvasPos=(25, 50), cellSize=(7, 14), canvasSize=(80, 25)): super().__init__(parent, wx.ID_ANY, "MiRCART", size=appSize) self.panelSkin = wx.Panel(self, wx.ID_ANY) - self.panelCanvas = MiRCARTCanvas(self.panelSkin, \ - parentFrame=self, canvasPos=canvasPos, cellSize=cellSize, \ + self.panelCanvas = MiRCARTCanvas(self.panelSkin, \ + parentFrame=self, canvasPos=canvasPos, cellSize=cellSize, \ canvasSize=canvasSize, canvasTools=[MiRCARTToolRect]) - self.panelPalette = MiRCARTPalette(self.panelSkin, \ - (25, (canvasSize[1] + 3) * cellSize[1]), cellSize, self.onPaletteEvent) self.menuFile = wx.Menu() self.menuFileNew = self.menuFile.Append(wx.ID_NEW, "&New", "New") @@ -547,6 +534,77 @@ class MiRCARTFrame(wx.Frame): self.statusBar = self.CreateStatusBar() self._updateStatusBar() + self.toolBar = wx.ToolBar(self.panelSkin, -1, style=wx.HORIZONTAL|wx.TB_FLAT|wx.TB_NODIVIDER) + self.toolBar.SetToolBitmapSize((16,16)) + self.toolNew = self.toolBar.AddTool(wx.NewId(), "New", \ + wx.ArtProvider.GetBitmap(wx.ART_NEW, wx.ART_TOOLBAR, (16,16))) + self.Bind(wx.EVT_TOOL, self.onFileNew, self.toolNew) + self.toolOpen = self.toolBar.AddTool(wx.NewId(), "Open", \ + wx.ArtProvider.GetBitmap(wx.ART_FILE_OPEN, wx.ART_TOOLBAR, (16,16))) + self.Bind(wx.EVT_TOOL, self.onFileOpen, self.toolOpen) + self.toolSave = self.toolBar.AddTool(wx.NewId(), "Save", \ + wx.ArtProvider.GetBitmap(wx.ART_FILE_SAVE, wx.ART_TOOLBAR, (16,16))) + self.Bind(wx.EVT_TOOL, self.onFileSave, self.toolSave) + self.toolSaveAs = self.toolBar.AddTool(wx.NewId(), "Save As...", \ + wx.ArtProvider.GetBitmap(wx.ART_FILE_SAVE_AS, wx.ART_TOOLBAR, (16,16))) + self.Bind(wx.EVT_TOOL, self.onFileSaveAs, self.toolSaveAs) + self.toolUndo = self.toolBar.AddTool(wx.NewId(), "Undo", \ + wx.ArtProvider.GetBitmap(wx.ART_UNDO, wx.ART_TOOLBAR, (16,16))) + self.Bind(wx.EVT_TOOL, self.onEditUndo, self.toolUndo) + self.toolRedo = self.toolBar.AddTool(wx.NewId(), "Redo", \ + wx.ArtProvider.GetBitmap(wx.ART_REDO, wx.ART_TOOLBAR, (16,16))) + self.Bind(wx.EVT_TOOL, self.onEditRedo, self.toolRedo) + self.toolBar.AddSeparator() + self.toolCut = self.toolBar.AddTool(wx.NewId(), "Cut", \ + wx.ArtProvider.GetBitmap(wx.ART_CUT, wx.ART_TOOLBAR, (16,16))) + self.Bind(wx.EVT_TOOL, self.onEditCut, self.toolCut) + self.toolCopy = self.toolBar.AddTool(wx.NewId(), "Copy", \ + wx.ArtProvider.GetBitmap(wx.ART_COPY, wx.ART_TOOLBAR, (16,16))) + self.Bind(wx.EVT_TOOL, self.onEditCopy, self.toolCopy) + self.toolPaste = self.toolBar.AddTool(wx.NewId(), "Paste", \ + wx.ArtProvider.GetBitmap(wx.ART_PASTE, wx.ART_TOOLBAR, (16,16))) + self.Bind(wx.EVT_TOOL, self.onEditPaste, self.toolPaste) + self.toolDelete = self.toolBar.AddTool(wx.NewId(), "Delete", \ + wx.ArtProvider.GetBitmap(wx.ART_DELETE, wx.ART_TOOLBAR, (16,16))) + self.Bind(wx.EVT_TOOL, self.onEditDelete, self.toolDelete) + self.toolBar.AddSeparator() + self.toolIncrBrush = self.toolBar.AddTool(wx.NewId(), "Increase brush size", \ + wx.ArtProvider.GetBitmap(wx.ART_PLUS, wx.ART_TOOLBAR, (16,16))) + self.Bind(wx.EVT_TOOL, self.onEditIncrBrush, self.toolIncrBrush) + self.toolDecrBrush = self.toolBar.AddTool(wx.NewId(), "Decrease brush size", \ + wx.ArtProvider.GetBitmap(wx.ART_MINUS, wx.ART_TOOLBAR, (16,16))) + self.Bind(wx.EVT_TOOL, self.onEditDecrBrush, self.toolDecrBrush) + self.toolSolidBrush = self.toolBar.AddTool(wx.NewId(), "Solid brush", \ + wx.ArtProvider.GetBitmap(wx.ART_HELP, wx.ART_TOOLBAR, (16,16))) + self.Bind(wx.EVT_TOOL, self.onEditSolidBrush, self.toolSolidBrush) + self.toolBar.AddSeparator() + self.toolRect = self.toolBar.AddTool(wx.NewId(), "Rectangle", \ + wx.ArtProvider.GetBitmap(wx.ART_HELP, wx.ART_TOOLBAR, (16,16))) + self.Bind(wx.EVT_TOOL, self.onToolsRect, self.toolRect) + self.toolCircle = self.toolBar.AddTool(wx.NewId(), "Circle", \ + wx.ArtProvider.GetBitmap(wx.ART_HELP, wx.ART_TOOLBAR, (16,16))) + self.Bind(wx.EVT_TOOL, self.onToolsCircle, self.toolCircle) + self.toolLine = self.toolBar.AddTool(wx.NewId(), "Line", \ + wx.ArtProvider.GetBitmap(wx.ART_HELP, wx.ART_TOOLBAR, (16,16))) + self.Bind(wx.EVT_TOOL, self.onToolsLine, self.toolLine) + self.toolBar.AddSeparator() + self.toolBarIdColours = [None for x in range(0, len(mircColours))] + self.toolBarBitmapColours = [None for x in range(0, len(mircColours))] + for numColour in range(0, len(mircColours)): + self.toolBarBitmapColours[numColour] = wx.Bitmap((16,16)) + tmpDc = wx.MemoryDC(); tmpDc.SelectObject(self.toolBarBitmapColours[numColour]); + tmpDc.SetBrush(self.panelCanvas.mircBrushes[numColour]) + tmpDc.SetBackground(self.panelCanvas.mircBrushes[numColour]) + tmpDc.SetPen(self.panelCanvas.mircPens[numColour]) + tmpDc.DrawRectangle(0, 0, 16, 16) + self.toolBarIdColours[numColour] = wx.NewId() + print(self.toolBarIdColours[numColour]) + self.toolBar.AddTool(self.toolBarIdColours[numColour], \ + "mIRC colour #" + str(numColour), self.toolBarBitmapColours[numColour]) + self.Bind(wx.EVT_TOOL, self.onToolColourFg, id=self.toolBarIdColours[numColour]) + self.Bind(wx.EVT_TOOL_RCLICKED, self.onToolColourBg, id=self.toolBarIdColours[numColour]) + self.toolBar.Realize(); self.toolBar.Fit(); + self.SetFocus() self.Show(True) # }}} From 61d53c818c0d291ac62cb2e3fa6c6f7924b33580 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Fri, 5 Jan 2018 21:15:33 +0100 Subject: [PATCH 063/148] MiRCART.py:MiRCARTFrame.__init__(): remove debugging print(). --- MiRCART.py | 1 - 1 file changed, 1 deletion(-) diff --git a/MiRCART.py b/MiRCART.py index fe2ef01..f49bca5 100755 --- a/MiRCART.py +++ b/MiRCART.py @@ -598,7 +598,6 @@ class MiRCARTFrame(wx.Frame): tmpDc.SetPen(self.panelCanvas.mircPens[numColour]) tmpDc.DrawRectangle(0, 0, 16, 16) self.toolBarIdColours[numColour] = wx.NewId() - print(self.toolBarIdColours[numColour]) self.toolBar.AddTool(self.toolBarIdColours[numColour], \ "mIRC colour #" + str(numColour), self.toolBarBitmapColours[numColour]) self.Bind(wx.EVT_TOOL, self.onToolColourFg, id=self.toolBarIdColours[numColour]) From 5350ebd88179efef39147608ec9787210db88aea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Fri, 5 Jan 2018 21:18:06 +0100 Subject: [PATCH 064/148] README.md, MiRCART.png: added screenshot & WIP/pending cleanup tags. --- MiRCART.png | Bin 0 -> 30019 bytes README.md | 8 +++++--- 2 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 MiRCART.png diff --git a/MiRCART.png b/MiRCART.png new file mode 100644 index 0000000000000000000000000000000000000000..a803fefefcb79a42a9910309d7d87cb6e6205357 GIT binary patch literal 30019 zcmeIbc|6qX`#@5{2xlj|pjSG(9$1O`a@52m$Dx}+(FvTx*R zNmtlt+Hp7S?$SM5ZLuF47pcijJw9@^uw%KQ*dFS(nv^8Rt=p0PY{xl|6H{nU*Qlu- zRdw#SJ#HMaX{6d?4LNGIH=;TV~5%bEIc#P zmbseqb$q%>Y`@Uea#NGcL$_8^QhHPOJBfO+TMcpU`S98(XL>1bi!|SEp4t_u(_%g) z;{mt9pQJ8ZRbKUayQU%R`j*laDw^eNr(eB1;Exh_w=uZv?nE0@KD0O4U|$TEWNP!? z?OV0(rM+K%NnT%kBHj)>Ldlqy7}FGN|anG{ad&Fu)hd4wmu{BwD0PNcOQ>(NWfIW+nBuN%+UpTmt9uFx+# z7=UuSJ^HIrfE_uPKgZrobbPh7uL2SHS_7&I$4~ zH8M*tnilW!(md0mi@SWK*pM&prMp>Y=*<9C@V|*qy$S<&H*K(aI#5?Qx;SSf$`aYBKg z>>X-BHL-QiNxS&z(3EG5k)3&C(Uo%%$Nc)W55`6#>9U@&{ELD9?YYLo2K&Z0w9fb| z?x^=iNoYA0pG>5P$_#z(bn_LdncCpHw4i zPf}k$^GXZBodFE3n%>nD+BPOVVFc-CQ`&7^$2a$SL!K6B*ZAacrx-cE#W_kCV~3VN z_H*!>@?+t%x@%HBUfySYMj5A_H{lAs_ppY`A5~)Zo9Ss4T)(!PJoMa4u4I0C&Hi2K z!lHsizZc0nez_Dk;}usT={xJPEO-@U|EKQgCB;{~cU{)EF&gqw^!9Cr-#GyL?^x{JHvW;sd&Pz>qbr-_#~9pm$5y=|b! zdOLC#AxE*UGo?uN3KtJ{DW~#f#RJ9)SFfr0fX364m6GaCo!33+8Yd!A6~f*3zE$dT z#}>F-i4`%n%9|VJp1w|5Hn-gHQ_S-2rxE5)c`!X5!v#BIyyQCgsjm!&o|`1H+mj$M(TojCZ;{%CQ)vwe1L)& z!A5SvU&P7Oxn#(i2kG3U8*druUb87C_vNsTP|Ovw4{{q30t(gUdo44!Q8&$QahL`@ zsd#buX%Y(9q&HKPcyvnsPM-lIp4To-Wh(6|hSpJ~2*GwY1Gp9$D z$0zW4i+q5fK!>{FEavyDk68ovF4XD9d_MFl>HglNqB{an_)CaH>?a<~1a*5PuORUz zHxkuZiN({8`BA2ZxdZ9F)0Ejat`x?24+hUjqD{6rw>039+0GPCBQ1R1U7!kNOt=>% zb4KEZKq$S)nd2k`6{*3sSTcCJQVq#*19P2A}y4c}~!{x>KlNOa8^?MVrO-t4w%E1#9s$I_^ zgn$XwZX_$98G!->UV55?X{Kxc6AJC^wz8PZZi|qP0v55w2I&1@*<^y zbHN!)1uECtkQ-lVJk=E{-p*aXcPw1DeOv{n=-K1rMIEHL54pF|RLk)F+SojzX}rj2 zBwigm_{c#E-@ZbCSHR6v?|#zRa-O91UD%J9uv*bcS3I&5k+6iJraY%r{%8 zDKXnfno2@V1T^#y;Zh;JwFxKbHdOlt)a}T~QbVk367i)RdYG}EduOScRVtr9mB6YZ!lZZzv z0w>NdH>eb^${(=7n;jP+ms)S^TuR)NBBpA+$E`wLfVU%Aq~>Dg0|PECi#S@*5`<}P zPos{uR}VSHl{%?i(32d*?3CfBkA`7V1&N;2Hrl5}R0=US3>x>=q?~GO{!2!-kzw2z zDPX0_#* z<2mK0J{{w1V}4jf<2*F;)|{QsbE@5k&*jrKI*rP;mb|)#=llU@*Gtnj;7R!)K6csY zOkXu|<#znEB=1Tr(cFgD;rB&Kkq34dri222e1{=~X#X~*9n@Gb%Te4fZM%|hS^Y@j zEyb~SLF)}t$lT)DE@jH5Gev=ZePS^)Q>x;!?GkZv0##2=#)&Ly8s)A2sMaG2;?&nhnBm34#4NDg7COy7rAY{ zn$-NxE1Q!8A?UW#T2CGjx4wqn`Owcc@Q!UB6i3er`CZP;&d$!r7<{(Uaa`CgUdp@a zaX?+cErOVA{kl&A#&rAcsHbYqlkTSeFj$AI);(wvJQS?TxfSY|pOP#T0K=uxabx%+2!&5| z`J7&1wvp`KYv3NKHrMLnMvt2@$ZJ}{&}J_5(v-qQg#@pQ58+^PkPt0k;%c9o_dB9H zYa4gZZ76)*4zJ)}AcH(?xkYOCKN}qsfu$yjU7(T^58Y zCY(rBYuKyiK-tdDn`;*u0>;!)5y2FWqBmuHQq;h%_R@ap7K$dvNh6I2Cqf_BDt!A{ zMZ$EslcO6)!H6MRyC__dP-o}FHj>liw zCwEz~Q$5%(CCeel0=pdb27&JEZ3hO0O>mv6I1A&z=VO{hENG{o6i`VB3aN6Fc6b+_s5sPV|(7 zwtUxZPihqHHQh9y$)MLK7FuY8<##(`e)F6bG1YVF)RZ>#TkK?tu8c>JT)HOQ6`bGc z+PP5P;b%sj8C5)OwD+22rZ{VCrrOr>t(pZ>!H$!{?A63kPiK;IV*VyAb+g?5)ZDDd zscW@br1KPBgt2SM9e&xuwiP|8xkGtFGuB3VomFzlR-0*53bV0HVdm-84x6+ZC+fS3 zn^)QSJpeJ`qahEEWHnyM^|_l}F|w3f&;D+xMf0wA`q+)MwD;$djVf@CJ&pui$$rCt z54aK!ifeU$oTH!BmWwy)#k%uY?_G#JX^4GD@uav$W!Or(b*NYo&!TX#;Qz8j@Xp4l zpRLZbTDP#BGNct)cq zjcO2k^4zBF=a>vn<>+!g@^5(i>V^UO>QjBxA{uk#^|eJk==Qt_+k$seL^QKZaU?hi z)8skSQ=LXVmd&`;&wnvq&9Ocs#3(g)w)dFVY+bbh^+nIDgoIm73@>_CPGR_(Ifoy2 zNy}ODwEkXhd5h|RV@#R@#l%@*HXMzk*_omnRNK(~Az|D?M5o0A_~~wU#VZ{>42E5S z+gN4R?lV~e#Lgf^uXhN3kC8IF>2_(aoGcfkOQKE&u}9)W9R>=I={c_!0FlwiKePi9 zZ0Qq~3&gYgqy2#zU{P<>+yDvpKMK0^p5fkln~8_{YR)82DzP&g+rh}Uwo{z7 zCb^Dtfxoxy_;O(&75!mqO#@p2&zTuk9Ly#cMGVnnu~9Rvf^DPC4R~P`b*OL}*z5s?YR~>B zHcf)W-JJQjsUBXc2T^LJK)e@zjA)0SX(rMrBxZ?*_@vQLPS4PNtk=xFa&d7&G->Y& ze)1&eHc0}P7;{D&J>`ve?-L62>h3okBOK$DAAe}xHmk_&G5Re?VMep0rJ-OZp5JpU zy;{Coh%`Pc#2t`gR3X-2;$CN(-{x7p|;2H zBk8<>a?^w8#${0gL{rZ*WfJ&-bvH{mS~9#ndgMs1W8tP4!795g&V&3>F2&mU6Y9*# z12OcAHLo8@ZOo}Sz7%6HapX3t80f>$R}~okW}nSZ9Q|rs_Nw=9hz)>TVZQ;Sx!yZG z@!)lGAoPC}^huVBxWz#(qHz(pT%Gx(fXTxiUl+virvgrekxvfzvEFh#?TrD+v%`Vp z`dK$<$h~P14U=UnDS_@P4-E}f=-PE9f*D3W?3)h&CaG<>xn-zTC!=X>qCYH^G}gSX zicuGaA4?A-Ikn0(%#w|?@`mc=W|uADRGhuM=54akiO?g59HVLV(q8XPpXH8cnBqsr ze296pMI~C+4^-AD+2UzQiIhVON|kFgUJ0Ru3w|!XBVh~k8r4RzKVuO)pFH`qHjTrv zOf0X*H%cQhI&0$deZ|3+6|sRR=T`f&Cd-)VNpogFi67o2*wJq{4nM+)?!AXqq_4!d zlNWIw8qUOc4(46$7`)D@FxA|UH?9#8&1fbu3e2u0KYn{zuKM6aAU1Dk1Ti}z$PPFv z-i49x97;`~=wO1P< zjjfZ0G+E30jrPXXARi3!NNL4HY_r#rvf)Dtr+(ekZJ9HZUG`|(q61{E1^Jb9wCsgCRg)6I*V z`CX@bkS}R@+yf+R!=u2C2r6+TSR+uza^!Y3DXaYfd*H#YLWB+aTyf$ge?fa6x*@vn z)LHcr{#H2>ebT7lN-cv%jd5o5V6r>PEz!2L5IG4n)jcr3{Z@ZU&FZ2g6lKQ8`N5LQ z2bEu>+e#{%@{<`IToPiBa$fQnT=D>c&8?<|am+n#tToHBnm{U;=D|-hI!LCs}H(^}A1 zh_vXLIFhRylUq4C!Rwfyd%x=5~5O(dD&Ta+zR<#vkWMx2B0G&(KJ&gbUrv>)?C zDuL^l)Z5%o1O-jjyey!n*m2+AbyVLbxDwVBTN~>6vj@ZY zvbz%br<-RwVz8#ul%ZVmrl7Wo_btwh?yzc?`h04eMT(C@i!^>Tb!-*MsTM8o$ShZM zqw;%w4o$4z5S*&z2mn69ahaO`ZhqPu)hbz<|04EPTbPVah1p=vXvfJ(hiW-@`w@>8 z`N&~mlVA=Lx*tK`CM`*x;AvG`+oy)5C#PTZJ7;|9L=b9;as&qLYDGoQq#1e5(lFCC zj)+-m4~0LZ=eP3$1o_EE^U=ar&CXG#NJVi%5ZY+J$Yvw#)lds91((7HD~`tTBmj>& zbqIJ&?ZJ=BKnN?n55b$we(;J8cd-(9B|7^HC(v?+ye&}<@-WrSz{AqwLj4_$?Ao#*lex

Aa89p3pvG=*N+O-gsxZX1rI49dLCcR-L!@1T^UMH=xGYfcK9 zY0jDV*@%-qiRGI~Swu6t^A%LcVA|8!486^f?8A$U+!R-uNtCPkSLAudWBu)1n{vb*Ez3M zc13%er*q8ZnqVQ~R7)ZS-6T{n^O194dJ})QR(`vxXSB$nzDLaRMA`mzcQ@(`9JaA>H-T#) ze=y6BQ9ffGJ#*cZNoGzw5;RS-qx5n-6|B~!Ps`Zxk}v8gAcU^(x8aXF$mN*5oc0C zknIieT`&?~?mTr^@AJ!|YNV6($Yi#* zjhK`CESNGlG<;YiF2ZAVZanYt9+Gup?WY8gM+7V6z1kGUUIn`VifJA0Q>j`8`~${WX9cjr8var2c@zWy}aPVw&b7^V=n@ypI(W?K9% zD?xfC!t*VzWNN?O7TFc&j3n^+Z_lyk_sJ{xa^?Ci6%-`ixW4}MbKA~JZsu4=Ob}`8 zee)uBMoCH-J&8wRhT)DM#rbK?$n@{IZC*7)tfhFp>%mX@g_+Wdg^JCw^AYJ(X`ZKl0H7HRoUQufqUN>qWd04&>n?q#5&C)hlW~r; z0!Fh@7=y}=A9?SmIEJ#8zs+R4ON_K9X<+gg&ndKCrk)b^gGeeJ52@j7T&_h+RxP^Q zQencAL>(b9qqOPiXeV+I<83&8n8Q?tj%tzL!cX>1Kl!FLGznF5T;$OX6&xTZ8?KE4 zoq);OT-$=_Qjt_qYbVeeb+W~7CYI?jhMs*gwu(U%;B2m^QSbx2Xoo=x({O}z!d>xp zaF6;>_=c0ods<%^N+bOOD6-5|P5Qlz&fjq=IIZj4C*EyU!kGb+7b}5GKHwOXW?ne7 z3Bn^4=#_G=y_!FbN>%jqysd~f_myDQZ6tNZ-{jp?aO$RRzGu5O#~H_qs5kEpYh$F= z+HL8U)5>A!aIeh2cXDwu#fzFdL@It?95~wPMcv?tR*Iud2`S8^X?tW$^{BN7njDGh z^F_~yfw^)=iitEbcj~F?kVhq>MG84J!avf&erkJGNsAZlhBn=g8$C;+%=F!)4rF3< z*0-p3=XeK8cGZtn$xXAPhx-y09D9P8Q$37IrfZ#ho98HgCQL(dCKe$-@jQ(ASvz_b z!<>jp;l_ffGwajOJjU-7lG^C_>2)kOdZgoxT8|fLID$0W9fcUmn{BO*q31YM3ri&^ z;ppyEBzA;Lno)$$?+A;Y4e5`i^le~P<_#(1@wAWC&9o!iOHH*$NUt57+Q@=YHim1K zdf9!dwDii)vG=E}Jn<6|JoepL2$8f~Aw*gjyIFBU5Ms^xr!-1&DSXp{CZ4|U-IHqq z8b)(d?~`1_FjY%}t0<$cLxt@8qMNrP<6E4_gOo#qw*>OJn1?1m^0axqpFra$aM)z@o(M642KxVA`B8+WRv#b5rs zudvzteN=<-SUCUGwWMgqxCFoB#}e)7JK8zucN>^ZCe!N_y=t^)Y2~D;QWI_W=0x-- z_5vry2TZ|+;3^~$jpijE^$WNedS{5g z8ivYDe_EomcUiFQLm+pJ5g_*yoxckLx7e%=h2xAG@Jhv%1T_%We(mbb%`4uKU>QpN-XC=Hw{K8C}nudtE+Op)hBuqgw zbF9^&8qC?_Ti23(r(@ijvvjoNx<1xw$#iAdl=L^#X_MSc?>2;&+a>gfEJ*k-#N{#> z<=TuVxprP7VOn%YfFT0dC~Oa&(H_eGlFGH)HTAK4g2zyJb>0wJ8$Z$N;Mb6g9_mq` z9aH>_jhUsB&|MPA63F#tLoX5)GB0#vrE`GuVR9L=6e!05&Ms#b?cQ-mNXky8V5-MF zh&P+gg9SjBJ2X=oAdPfxSN7|PbU1Ht(j=NjNJtF2jiOOWBW;=_7e@NzAxnDnx}X-k z^I=qO-+g|3w>QUt=5si;*eUP9`y1Q{7A>iD(I5~25v9D$kjt3>2o8Vix2 zz2=heWO7N!Fnq;gn0+yM?evza0syn?f9ZcOg2|ie@Y7b9jsv7;?5UslXz_>oT-%WAi$0ROBqUB%){I%;ZZ}P!v`)p7aGu&N z)p%a4%Ns&~Y7}F-2M|7`=6>pP#Zf_o{Ge5dAI4+!e#7iXZIj_L^Z?e>qxVUJBAx86 zFqzgdQKyZb*{$t%kJMI`OL7=kr@el{QKq^scxbYv{1A5VK_OMXUyVA`karH`!laa4 z0uI4zJfp|5lyGNh6J?A+PoG1URLr5S0*q%}R%xpPm6&^LmX`WhCoV~GZP@5^&cNII zud+NwB6^Im7)IOSH0wMe3)h{S^3K<>|6{b6Az;F89LGZ+zipE1#AR=@y#d)WnO$wp zB#W8~s?JFBAFP|%zw6ifXnFTm-kN*;TS~?s`TL{9)-%1c5bKn1x%jmH0=cX!Cjh^oiTKI|7lm=5OnQn2qPsN?CKymi!35}sQC4orp#CW3}ITpjHZOa|F?pETL z-hjbRT50F@DbD&_X}~aEklM7?koml(+QCeX=Xi~30e#SpYd!OhUE8P^+B3Dw45Pf6 zmZUh{Ud=ytKgO&42oI7<5Jb{_KLp{Y72UCT@@}U2)IKDK?er+YMq&o-%;y=G&#$CG z1-!V8z;1jtXOE@($;m9$aAIC1srV+@yJWYx}OY4Lml~)oa6K+CCKtulmoK@85`&2bd{&V@$H;?&#)dQ@B>>2wUgvzZ}=x&;nKu4+63fAA^7z&t z*MWi0)C4_@JHo2}x>0u)Wyy%DArcP;(FW zC5!&@*?frrag;)6iXLt7!CrcfzV{FkuEVX69alZZ`N-ahst=k{{q4PU@~g*t&Kt<9WbeW6c|%`HJ{;$_`&Xo==g_cNuGYaw=$$cEbNu8YY;>1=yN(KgWH3E`VA(z5R0?= zRwW5#PxoO2q*k3NgZT|+f4O>p~e&{{eO!TcF?ZY~KNtkO^ zh;&{P_AQ5-uqIXF?%U->yN>U>19e&(HXmVSS+6jt#~WeETvNl&=AyDJFUQH<{$c}= z;mqotmy6~DeLe#zL0%OnT;e)#2QzAbtMq{t#gY!!ZIq<)>rPm>(opYe&~bjXM$2Kb z!cqa+Es{%Fa?(rAgCiy$R5-#?1=BV&amKs@-Vqotqq?V$XGMusl|G@tLpNdDUI&d% z5FM9}H~I8MaX%h8JM^qG@oA8;Sw32mUYTFA;syJl^SLI z>QGOn3n5VL+dTM#w(tkJd{H$C-3@}Ff#_|JAzaOwdzLKif$>y+Lzi}Sj?G$f^lY+p zRMz-&K~4c^ERnGNO$$cR2anpvb+~*nj|IlRzyuaT2Y{t99*iIqLFQy4VjrS~4y!uJ zcy)XlP$N0#=;=Ap1hNKm2W1g$)P`ytIg|w!-MeA@02chhG{)hhqMGHDd`W$8o2WGM zmw9*q!u(V#2ny~3lAOc|BRa*v)g)kxxu_)T>2|{pb*PgIw6ydaU8ZdYU$i8{s+5L5 z|C_RG(`j1+EmsB)#v>H+(IwD#Aau-4X{RE?@Kd-?P!_EAO zC>Ee7P;R$0QuXK;!_(^!CSHx%43hhL$!eC==Pjs@3eesG;L{fj0d|X(o1Mt{4i3!SKcb^*e1Qc<7yvjkq9h zwab!uLrCZr=7rr07L`6-%`zq)Hs@B@1Vq-8pK6-Lb@E_f>hdp6Y?+s zm+Q&~Tpm?$d-NtfsLcaHe;sZ8+UBU$n^H zad~i!;R#g{BUQQ8Rd(tpR6~g|yvq=KcO($>C;P1TX}m6~xpdb0p&MjD<_A&WxJGjp`2PpLGwELKj?}U0*l_9DkNeH``OSM3umfHP zfBI752O*XHTLB!HX(rWOBtMX9yQap2@$U|iYMb{hVMj@s2gfqP14Z4l1Aed3lrvuY z>Y;6)frEO)P{LEo%I7@-K38sUusOe6|DbQ$#siv-6=yH4)CroSwdCWew^(-a?+oK6 zr!%h_z4x{^m>3vr&2b=40c_XN(yzUkX($^X;JlSkDiZ4&A0Rz%+3F@IR&}km0jR@g zD|i}JyDIF4XhMWreIy|v_j#{@LtoC@CtIE>!&r7!R7G>0RQkp*h-T7bjRKChr09`e zQXOHpP2QqlDdeU~5t}^n1&jU*>_yYMeP^%i^0}~&*I@E8&y%MSPTAYfNfJM%cR!FK zYARPh(y3Mk&lhX1So`wwdUMH(k6wnCG;U7dg9+?SHBj1Vuszu-(@-pmP%5|Dsxzb} zk5C#BsFxK8VA?cZd?%2$RBf;YCw7f6&dNvQ=!RNNd221JTH@*PWSI$L$R?h`-YU5v zHeq>$f&ZM(a;6fFto?WNyqdiA&dY?liK#lJm$tstQLw;`qAy3{dy0 z^%pMIvW$zEQM_ZDA2|VdRtDKt7%0|F98yTl`OvF3V>kMpti4ewT3Wwn0*bkSUff$| zZ3v2e6Ej9uDK)!T?8jt`%*Q6gbH8`M7Aiq-DYm)cYuns@?`{c72k5XFCCvqdF3%0Q z#svuKPx_ZUvnu*u`0kOo>)Zb&^9xr)Fl>*PYp-5MPKfz;6q>CL2LQdKuFLVUh~|u4 zy!pU$94?vx)3IZ@5Aeh!28w^wx{nVtPc(2FN`SpNT{sRN%uwOV}z=Q>gbnR1jGDM>^ zTp5GuK{D1oP>v6=o&SLxK!R)iImi2aTv8h^#B`up3){Zsz`Ztlb;G7O60i?-IEC!fh2}o$1raU+B^$;nfRqaK<=)(G*qtqi(;m`^b#9bmA zI?i4))msp0+QsU9RpOEy_=9a#AfNu9Jk-a|>Xxhku>yi8G5_L_K+xf*sBwTGmCp}+ z0Pz7(TmU2XbDcK_VaKU&`4#nuD!{FdN4SYcg7{>UM@XnUy}4YxP6vcw6BzMf{&-SP z^i4@)LGpZj0Lhc@!}?{8O1N+#C`N4b5#h}zBq)=60pEzwv3?AA0wAM+bac+Q15kJH z^a0bKcyND=uMSQ?8E^z-|jmxK|@Kj2mU++5z!L-Q9gPkF{O8m_ILi?2}Bg8~if zpC2%$!AqZgPFa~x9))}tpyMCDtM&Wy==ZF|VIoL5eIpzSN>vQ{=cW?EqCa2!1Nni6 zLO##>=LbyKNow@i^9)J%Z4e}Zfcpm|2|YWlZNZ|eGi6T={2@Q(HjhnqoSPN#<6k0m zb(DOD3y|;T9ETn-zrU885D8I-e{X0F)0@F6#`$ai;#10mmB4Iir2vdS|BgZYLC60k zg99^%bEx*${xbU${&Hc%*Zu-c!A?SO{3O#QcG|K{H6|CjRrR8RWuQ@UtG~uTFjQ(Sn+=1-mz- z6oTUgeCG$48Il7g<3owZqSvX!MXZ6y$AJ%4yl3NLowtGn;U~ysKodv1`Uw*!yZ81Y zI#w`1FQ5X9v>zCCpv-xV&PT>z`Hh>Rb?};+G!MkytsmFQzEM-996q-n^Aq$AHpGXx zfwl*odI^MsN4};!bn_a~Qiy{1rJbyootybynVtJ0ICTYzL2nlt5Fk`8hOlRIZ^kM~ zLXF&Nz^djFrT>&Sz(FZNJXg9aJLLI(C`72wQzT@B!S`AP%Z}HFUX}*i%GB6*I-ZAE zGf0mdZwCjqU3+^0`7^WVy#^uC%^Cy%~y73(~V6__*F~f_;c>{r_Na<_83LW8zsX z`Cm5FUO7St*(Q9(%Q`dMF0bmh|BbPLrT>y(-_AAUgz*1?;^x75scgzIf7JLYC6Iyt zL=FJam^;tQhcsnuErbKso&GAvsaM=kc@|EpHu6}cELhse1bM}`SeA0_pTn~Ga17e) zUoFU+zbF{Y>;1%x1Vm%c_Puh~2{Z~74YyIV;>2VqQG|53M;t_;!2a*@?u{ z>d)fA`&z;C@H8BRkpHe+!Mg7UUcKlM*|GU(=kEvLxbyq@niM*$uGFvR%o4;~tSZZ3 zO@KfP&zaTA&NY#8?1z!3(i*i$z{GP$keamM|K_2|r{X$=|5qxUTM>^QKPo`n56@oz z>sC|a94AuSvUh@Be{pP78ae&EoeZ-yfNW+gU-)hY)M}8WG-!_d%Zv>O=vvkau!RHE zVa?dgH(|~D!>A5QTXqF1#YJ41XKDYgTP}zV#VmV-+2=yUu5Av2P59AQ)5rizqv}0i zX+Rtlo<>fYYobAR{JT_oeuV(yMH^uRoA_>W&ydD)&OdlADr*r$JF{vZvAO^_puQ7C z#^#$a^!c89&QA_OoR5Ve-_NgEE5b^#{%RT#1Uo0gF}Mp5BZ8+9=bJ$GfA~J)CGnE` z_$5IVfb=|V_;;T-7bdn5Dwq9@2lAyxX9mJcE)N#!jJ59a2LS&FJ=69g$e*3bzurde zJ31Eyf5u3Bk}OkV;pMvcg>Vh8+1>mrG=paR?KM-^ke7mj7R3XH>i!0#xo5l8%U*<* z@^oG-R3*Hgckn-1C#V^b{xozE1PZ9?QM&Sf_R!u{BGoQYu7AVX<&EX7c{8E4x!rXO zgFd`K7x@?HVvUUDSxxQ}aaF=?+6E<_KM85Yb?Nmd;T7vs3%wCuwl4aQaUWz22QY*Y z1Ki(&(Es9jf`pZ`qP!%RDBJ&uP-7`-hy<)fA1$S>8ysn-gnRyXLdE}mobZC~%?>ps z+-`UY<6l?g-;5OhcMzCg1pi60#)+slRE29c)E7ouxFUq(p9~cL1&%fc2U_M;{ck`U zkQW}Xc>1Uds$#Z%1eOhR*9pXCKAf+7nEQ=Yq!!02z5-183CxTQGk!p#=fgm@y0qZO zB{p&YWpi-NOhstvfgF{%PLS~g;2sB{j-JcdFh3h+)g&nIp3-A@6i%_;=UGoE~0tJKKRo3tKf%-`u zO_L8QvT%jpTwO{gtKRSJD_S}rC#?E8KoN<85@2?`{x+=$^U+^WuP8aK|}ibKcLtJwAo=e00E9d zd-0s3S?#NOxVh>$R!!V-mMsEed@K{9YMB@Q11El?i`$(jenIf7g2lP=#krzIma4%r zH)kD`Gu1!+LFNn1AN&JigXrwD|9)`L#P&ZR`7{79{F32o`n;tG3#%zHbG4PMib?{@ z&OlY=?=Tk<`w@Ev6Jjf;ORY6|$;c_&FFOzBF#s-1o-ea?u8=g^-0*obD2mTT!BxII z^Hsi=oey-8*L?jN%I84)Cd^l_ec>a}))hs#!ZSnw-dl4f5MJ&ByIxpT&#b)^UrpEj z%JzE8S=L6A|4SYCJbedc;H+BWKln9xWfm@V7I6sC@(L5b3YW0^%$4H(K~W#!HK=ji z3yR)B_Wzn#_aISq3u^n9EdgMKXDQLFYZItYW-FgtR&2;qk={zELyUeAXqm7wu3Hy4 zBeYZJI$SQV62~)TAX_j3&c9~80+tWnk7u3m#Ah~d@#qYJy9MvhVeKa>35J_foGZzi z-~X_%_#G1e+`bvTJp|qYix1a#sKeiaH{_I!gAEPvuR?Gm@U|w_jsq}^R!ZfP_LQ0B zI58t{HH1y;H-+KyChx^E+n%?Dp6`ll5&p|E_9{3a%^Q(CQVS+I3b6%ef#-5dfwP=Hmk#D~ugdZS2ZIu` z+mJMqToP3Z#sywj6FT3VgbXysHNQVW+`6#-dO1}bCzB1C@b9p%7K1ad;q#4Sr9Rdq z<%KjaJAYkDkG9#?urXgSd&E!!+JGQa443Ph#-}SrY?!qqZz!T1pXzr)+U5bD&ztx8 zh35#vu!4Z>AQlTw_Jy6Y3fjE!x8w{YcG&cg-RdmPGX)U*Kwloe39Z(%?;*{(ZV&o; zs>kajcYa`DoQoGdlz^te6|PRhd(qr*>IjA16^im_X`>P=Fr@4QE?YM$9$4|R9$p8W z-x3BKfxG8Dhoglp&lUSXHTJLH2HO7P7}Ql@$Q9gB2?YOZ4|tyaCj_V;3op;Ijj{Hd z;ngkCQesop1hE^W`_H1gOy36}u4V>)Gc1XgIb1t+Qo zvU`L5Ru5qDYa__-g>FtKs$Mc>+-#qKPUv3TnRpBAyFtR?;@cxt0k~l1^k(U7z%je8 z&=}=#8QOaD7~TetF!*owKKCH!TS{=KjEybb9W(wo0E3gOEv6YoV*J{ z4D4k=vk3A)b;1&+0q*9o``WtnS&baX=f_|fKt2BI_+9WdmcH6j3GoX&{>SL|VKU3lBnon`1*c1wEh+?2OdSSIaPCIe{i4}g!7K@8(eO-w z(RFrj*0wd4|7d@8g7X)&g?3}+CNE)xpZSMi{&e-IL|3WEyv;&AUU0_naVR>>=_^+~ zO#H8c-JclAv@aCD*{s(>!UlJ2fQ{%2XSqp?!30Z1?}fP}tb^ z{y+|FY;Tq95M;s*u#u6C?Zi?@7C(Kf$k!ugSpu8elRq^BHnwnxzI_$7z~k=T0oFV8 zYQvPJcTR8jCrEE>hb30{q5S;(G&EWQn}1iujOtw!e7Le%f6zMH%8VNLsnlAG)z!VD zUj_I-G|zT;Cz#$sH>GOX6-p!fDaQ}8#1q~Noe~Y%9^2J-Fev==5t2yz zP2AE7b}6#4 z_1-iKwzapnx3X$E$*)~NsLju;p}+4gCJ_P%B*a6h9{zd!1|RC^_4>gS#&sv(e(A`%}% za%|p~$iRL9tKpApEUrUaEZNxd_lOe2#KpzMY~(H;!F-?xgbCa^0Uq_`VxvD9=A6X~eRaA%iwm-i zAN^Az2(Yr|`=o@WhOfgo+g0RG#c-gYKjD5XwEvdcKs}g`+yK*I#s}_i`p?6hzx?S3 zcM;{W;?U@=Y~0F z@zFnEYoMQhO4T4I{pW(NsxpoTjF;_k$iFvW%Uc3OOHPTU7?;u(r{->Q{KH|F+AX=} zB3;vRv+0wijni3{`&a2^+&^Cv-mtB?*K*r)f6sBR1e53zaUo97H;!~JvYsTrf8n^2 z@7|mgx!K`D15HldOP8dQHirnt_uL!cAirr;tyUN`>j7sIY~}eH9@Z(WzT`MoxbcK` z-0=EhmEfn#Hv8ZQZK^*Uvr2bSi0ZDdJ-T)ityf+%cO#tZc-H&~Hf5O~haVpWClI;3 zxM&~+`Y`%JEypb9NG85{)E}FlviY)p*oBI|4-?kZryGmY4yFIn>9c?5N3U%0`)NCF z&;)&X>ozVad0Ud(5z~CoR&TBU#sN{}=JHAlM@Q9$dg-J`6|`x<=HIG+t=vu}a3O?^ zD0cD99Pw1g<11hETXH&MLUXgv%D*gsEty0w$CQWGqjG9Tlg`XO>RKZb+QKPoOluo< zY$9acf*vdNkf-Xv3&zqLm4~`MalJ}DQylJ0wE_=@*yc&-xVO`TioyXa*+~7%TaSm79MQ z^Ksg=Xa4akBDKrgOCu}xy!hokx8%tNoPp*+mjj7iIb4|5HkYix(Zd!ANzw=#-ieom zQzKjJP8Tg-(Wi^&JLIM8aBI+q$6;^rGmts~e^ZZ(7%#P6^J#7x24ZQI!v?ss8oJxV zlI$9jFRi8;44skD*TJemS&WeP9 zh)VA@{ahbg`os#8ezRUbE`eNH{<5-=@%Wl`T5G*jD2;vcg|t)|X4Apq?)Ihr^=5ku z%O9TgnE(QLtMxTavh2pFe$4GSU%v81hw*^(+p;ls$K_Ib`K{U~_q#8_u>13$;#_}z zN%iBJ(uXI8_a-#f++JTY)tudwtbja1#62>YPPyvVc|}87NuH~r5ug0%G6N9l(%6E7 z2>dRXYCaDZm$t}Ecq3)-{Bt2>pd0;U-_=R{)-6p42{T-3su}K}w~Js8eQobvJ8*oE zilJxE%Cl|(s7N&6sFKey?+%!*ub@CPmuy!zJzaIVl6%QFOs)dHe@y93u=;IbaQWDR zG(@^;Q6Y4*jr}1Z{P(j;`pw!a1)+OET(tPU<}%PlHUqWbo~=K^U5#7k1UNwlx~(R1 zLwjElhs5k?n6}KH@Hgc+ITR6hjCCK4j2MQ%+u>c`2+l@j>-lmkjFn4g=CJZyo8W*R zW!?H0`}ChKjD_WfCoM`u@w{ujOt(LRu8QdpPNE>* z&IpbA&umM-&O0gS+E>USg#_1x4A+;g@VhI0{=KNsCV#yGlg0-HHF)U$!4)5=BInAI zjE@J4Z%J2EJQnRG8CY^*L>u>DgAWgNb>RVg*UN_m(6djDH)QlBuLi}9`mXW*TLWdU z;iq1Z};_fnj;l>dndqu6x_TRy!iM|D0cE8}R z*9#`XcNhAj&@Zm)e!kUpIs;7OY(4fMR|SbMT6MA9UZ_`prxZ=IvoN?y<^n?S^b-3Q z7bR?bu?ov|RXK4h`V?HE7q!)|YK%sO<}JKO@i5bJ`ceH$KZo6B@t1UV9F2MQh)2vZ zJaWJTcf?n(uJBE3C;|E+0sBbiX&Wny0JYrms|X zwGVSs+T4wbZ%bv`Kr*ypB7Hsdos?tO6<_gOKZEz-F2&?dmdv$ z+M`+n>#bMdOM_t%OV)#gVwXd4Fc0?&|EFP(-(T71lAq`=gC)e5A!e)r2gouTB5N+`%5jU^}OoA)7DmrR`p|Gy8r z#Mt8kRj3c)IwVX%P1(m#d}$Ab6%v o8hKw7AIkHUnI<2)L}YUM%HL}`aZ$Gqpbgt`Wi6%Dqi4MTACysPy8r+H literal 0 HcmV?d00001 diff --git a/README.md b/README.md index acff52d..736ccee 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,15 @@ -# MiRCART.py -- mIRC art editor for Windows & Linux +# MiRCART.py -- mIRC art editor for Windows & Linux (WIP) * Prerequisites on Windows: install Python v3.6.x[1] and wxPython v4.x.x w/ the following elevated command prompt command line: `pip install wxPython` * Prerequisites on Linux: python3 && python-wx{gtk2.8,tools} on Debian-family Linux distributions +* Screenshot: +![Screenshot](https://github.com/lalbornoz/MiRCARTools/raw/master/MiRCART.png "Screenshot") -# IrcMiRCARTBot.py -- IRC<->MiRC2png bot (for EFnet #MiRCART) +# IrcMiRCARTBot.py -- IRC<->MiRC2png bot (for EFnet #MiRCART) (pending cleanup) * Prerequisites: python3 && python3-{json,requests,urllib3} on Debian-family Linux distributions * IrcMiRCARTBot.py usage: IrcMiRCARTBot.py `` [``] [``] [``] [``] [``] -# MiRC2png.py -- convert ASCII w/ mIRC control codes to monospaced PNG +# MiRC2png.py -- convert ASCII w/ mIRC control codes to monospaced PNG (pending cleanup) * Prerequisites: python3 && python3-pil on Debian-family Linux distributions * MiRC2png.py usage: MiRC2png.py `` `` [``] [``] From 845185f9c390e672ac9e6e967af709f7aa230616 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Sat, 6 Jan 2018 01:23:48 +0100 Subject: [PATCH 065/148] MiRCART.py:MiRCARTFrame: cleaned up. --- MiRCART.py | 517 ++++++++++++++++++++++++----------------------------- 1 file changed, 231 insertions(+), 286 deletions(-) diff --git a/MiRCART.py b/MiRCART.py index f49bca5..a8bd09f 100755 --- a/MiRCART.py +++ b/MiRCART.py @@ -22,6 +22,7 @@ # SOFTWARE. # +import enum import wx import os, sys @@ -290,26 +291,164 @@ class MiRCARTToolRect(MiRCARTTool): class MiRCARTFrame(wx.Frame): """XXX""" - menuFile = None - menuFileNew = menuFileOpen = menuFileSave = menuFileSaveAs = None - menuFileExportPastebin = menuFileExportPng = None - menuFileExit = None - menuEdit = None - menuEditRedo = menuEditUndo = None - menuEditCopy = menuEditCut = menuEditDelete = menuEditPaste = None - menuEditDecrBrush = menuEditIncrBrush = menuEditSolidBrush = None - menuTools = menuToolsCircle = menuToolsLine = menuToolsRect = None - menuBar = None panelSkin = panelCanvas = None - toolBar = None - toolBarIdNew = toolBarIdOpen = toolBarIdSave = toolBarIdSaveAs = None - toolBarIdUndo = toolBarIdRedo = None - toolBarIdCut = toolBarIdCopy = toolBarIdPaste = toolBarIdDelete = None - toolBarIdIncrBrush = toolBarIdDecrBrush = toolBarIdSolidBrush = None - toolBarIdRect = toolBarIdCircle = toolBarIdLine = None - toolBarIdColours = toolBarBitmapColours = None - accelRedoId = accelUndoId = accelTable = statusBar = None + menuItemsById = menuBar = toolBar = accelTable = None + statusBar = None + # {{{ Types + TID_COMMAND = (0x001) + TID_NOTHING = (0x002) + TID_MENU = (0x003) + TID_TOOLBAR = (0x004) + TID_ACCELS = (0x005) + # }}} + # {{{ Commands + # Id Type Id Labels Icon bitmap Accelerator + CID_NEW = (0x100, TID_COMMAND, "New", "&New", [wx.ART_NEW], None) + CID_OPEN = (0x101, TID_COMMAND, "Open", "&Open", [wx.ART_FILE_OPEN], None) + CID_SAVE = (0x102, TID_COMMAND, "Save", "&Save", [wx.ART_FILE_SAVE], None) + CID_SAVEAS = (0x103, TID_COMMAND, "Save As...", "Save &As...", [wx.ART_FILE_SAVE_AS], None) + CID_EXPORT_PASTEBIN = (0x104, TID_COMMAND, "Export to Pastebin...", "Export to Pasteb&in...", (), None) + CID_EXPORT_AS_PNG = (0x105, TID_COMMAND, "Export as PNG...", "Export as PN&G...", (), None) + CID_EXIT = (0x106, TID_COMMAND, "Exit", "E&xit", (), None) + CID_UNDO = (0x107, TID_COMMAND, "Undo", "&Undo", [wx.ART_UNDO], (wx.ACCEL_CTRL, ord("Z"))) + CID_REDO = (0x108, TID_COMMAND, "Redo", "&Redo", [wx.ART_REDO], (wx.ACCEL_CTRL, ord("Y"))) + CID_CUT = (0x109, TID_COMMAND, "Cut", "Cu&t", [wx.ART_CUT], None) + CID_COPY = (0x10a, TID_COMMAND, "Copy", "&Copy", [wx.ART_COPY], None) + CID_PASTE = (0x10b, TID_COMMAND, "Paste", "&Paste", [wx.ART_PASTE], None) + CID_DELETE = (0x10c, TID_COMMAND, "Delete", "De&lete", [wx.ART_DELETE], None) + CID_INCRBRUSH = (0x10d, TID_COMMAND, "Increase brush size", "&Increase brush size", [wx.ART_PLUS], None) + CID_DECRBRUSH = (0x10e, TID_COMMAND, "Decrease brush size", "&Decrease brush size", [wx.ART_MINUS], None) + CID_SOLIDBRUSH = (0x10f, TID_COMMAND, "Solid brush", "&Solid brush", [None], None) + CID_RECT = (0x110, TID_COMMAND, "Rectangle", "&Rectangle", [None], None) + CID_CIRCLE = (0x111, TID_COMMAND, "Circle", "&Circle", [None], None) + CID_LINE = (0x112, TID_COMMAND, "Line", "&Line", [None], None) + CID_COLOUR00 = (0x113, TID_COMMAND, "Colour #00", "Colour #00", mircColours[0], None) + CID_COLOUR01 = (0x114, TID_COMMAND, "Colour #01", "Colour #01", mircColours[1], None) + CID_COLOUR02 = (0x115, TID_COMMAND, "Colour #02", "Colour #02", mircColours[2], None) + CID_COLOUR03 = (0x116, TID_COMMAND, "Colour #03", "Colour #03", mircColours[3], None) + CID_COLOUR04 = (0x117, TID_COMMAND, "Colour #04", "Colour #04", mircColours[4], None) + CID_COLOUR05 = (0x118, TID_COMMAND, "Colour #05", "Colour #05", mircColours[5], None) + CID_COLOUR06 = (0x119, TID_COMMAND, "Colour #06", "Colour #06", mircColours[6], None) + CID_COLOUR07 = (0x11a, TID_COMMAND, "Colour #07", "Colour #07", mircColours[7], None) + CID_COLOUR08 = (0x11b, TID_COMMAND, "Colour #08", "Colour #08", mircColours[8], None) + CID_COLOUR09 = (0x11c, TID_COMMAND, "Colour #09", "Colour #09", mircColours[9], None) + CID_COLOUR10 = (0x11d, TID_COMMAND, "Colour #10", "Colour #10", mircColours[10], None) + CID_COLOUR11 = (0x11e, TID_COMMAND, "Colour #11", "Colour #11", mircColours[11], None) + CID_COLOUR12 = (0x11f, TID_COMMAND, "Colour #12", "Colour #12", mircColours[12], None) + CID_COLOUR13 = (0x120, TID_COMMAND, "Colour #13", "Colour #13", mircColours[13], None) + CID_COLOUR14 = (0x121, TID_COMMAND, "Colour #14", "Colour #14", mircColours[14], None) + CID_COLOUR15 = (0x122, TID_COMMAND, "Colour #15", "Colour #15", mircColours[15], None) + # }}} + # {{{ Non-items + NID_MENU_SEP = (0x200, TID_NOTHING) + NID_TOOLBAR_SEP = (0x201, TID_NOTHING) + # }}} + # {{{ Menus + MID_FILE = (0x300, TID_MENU, "File", "&File", ( \ + CID_NEW, CID_OPEN, CID_SAVE, CID_SAVEAS, NID_MENU_SEP, \ + CID_EXPORT_PASTEBIN, CID_EXPORT_AS_PNG, NID_MENU_SEP, \ + CID_EXIT)) + MID_EDIT = (0x301, TID_MENU, "Edit", "&Edit", ( \ + CID_UNDO, CID_REDO, NID_MENU_SEP, \ + CID_CUT, CID_COPY, CID_PASTE, CID_DELETE, NID_MENU_SEP, \ + CID_INCRBRUSH, CID_DECRBRUSH, CID_SOLIDBRUSH)) + MID_TOOLS = (0x302, TID_MENU, "Tools", "&Tools", ( \ + CID_RECT, CID_CIRCLE, CID_LINE)) + # }}} + # {{{ Toolbars + BID_TOOLBAR = (0x400, TID_TOOLBAR, ( \ + CID_NEW, CID_OPEN, CID_SAVE, CID_SAVEAS, NID_TOOLBAR_SEP, \ + CID_UNDO, CID_REDO, NID_TOOLBAR_SEP, \ + CID_CUT, CID_COPY, CID_PASTE, CID_DELETE, NID_TOOLBAR_SEP, \ + CID_INCRBRUSH, CID_DECRBRUSH, CID_SOLIDBRUSH, NID_TOOLBAR_SEP, \ + CID_RECT, CID_CIRCLE, CID_LINE, NID_TOOLBAR_SEP, \ + CID_COLOUR00, CID_COLOUR01, CID_COLOUR02, CID_COLOUR03, CID_COLOUR04, \ + CID_COLOUR05, CID_COLOUR06, CID_COLOUR07, CID_COLOUR08, CID_COLOUR09, \ + CID_COLOUR10, CID_COLOUR11, CID_COLOUR12, CID_COLOUR13, CID_COLOUR14, \ + CID_COLOUR15)) + # }}} + # {{{ Accelerators (hotkeys) + AID_EDIT = (0x500, TID_ACCELS, (CID_UNDO, CID_REDO)) + # }}} + + # {{{ _drawIcon(): XXX + def _drawIcon(self, solidColour): + iconBitmap = wx.Bitmap((16,16)) + iconDc = wx.MemoryDC(); iconDc.SelectObject(iconBitmap); + iconBrush = wx.Brush(wx.Colour(solidColour), wx.BRUSHSTYLE_SOLID) + iconDc.SetBrush(iconBrush); iconDc.SetBackground(iconBrush); + iconDc.SetPen(wx.Pen(wx.Colour(solidColour), 1)) + iconDc.DrawRectangle(0, 0, 16, 16) + return iconBitmap + # }}} + # {{{ _initAccelTable(): XXX + def _initAccelTable(self, accelsDescr, handler): + accelTableEntries = [wx.AcceleratorEntry() for n in range(0, len(accelsDescr[2]))] + for numAccel in range(0, len(accelsDescr[2])): + accelDescr = accelsDescr[2][numAccel] + if accelDescr[5] != None: + accelTableEntries[numAccel].Set(accelDescr[5][0], accelDescr[5][1], accelDescr[0]) + self.Bind(wx.EVT_MENU, handler, id=accelDescr[0]) + return accelTableEntries + # }}} + # {{{ _initMenus(): XXX + def _initMenus(self, menuBar, menusDescr, handler): + for menuDescr in menusDescr: + menuWindow = wx.Menu() + for menuItem in menuDescr[4]: + if menuItem == self.NID_MENU_SEP: + menuWindow.AppendSeparator() + else: + menuItemWindow = menuWindow.Append(menuItem[0], menuItem[3], menuItem[2]) + self.menuItemsById[menuItem[0]] = menuItemWindow + self.Bind(wx.EVT_MENU, handler, menuItemWindow) + menuBar.Append(menuWindow, menuDescr[3]) + # }}} + # {{{ _initToolBars(): XXX + def _initToolBars(self, toolBar, toolBarsDescr, handler): + for toolBarDescr in toolBarsDescr: + for toolBarItem in toolBarDescr[2]: + if toolBarItem == self.NID_TOOLBAR_SEP: + toolBar.AddSeparator() + else: + if len(toolBarItem[4]) == 4: + toolBarItemIcon = self._drawIcon(toolBarItem[4]) + elif len(toolBarItem[4]) == 1 \ + and toolBarItem[4][0] != None: + toolBarItemIcon = wx.ArtProvider.GetBitmap( \ + toolBarItem[4][0], wx.ART_TOOLBAR, (16,16)) + else: + toolBarItemIcon = wx.ArtProvider.GetBitmap( \ + wx.ART_HELP, wx.ART_TOOLBAR, (16,16)) + toolBarItemWindow = self.toolBar.AddTool( \ + toolBarItem[0], toolBarItem[2], toolBarItemIcon) + self.Bind(wx.EVT_TOOL, handler, toolBarItemWindow) + self.Bind(wx.EVT_TOOL_RCLICKED, handler, toolBarItemWindow) + # }}} + # {{{ _saveAs(): XXX + def _saveAs(self, pathName): + try: + with open(pathName, "w") as file: + canvasMap = self.panelCanvas.getMap() + canvasHeight = self.panelCanvas.getHeight() + canvasWidth = self.panelCanvas.getWidth() + for canvasRow in range(0, canvasHeight): + colourLastBg = colourLastFg = None; + for canvasCol in range(0, canvasWidth): + canvasColBg = canvasMap[canvasRow][canvasCol][0] + canvasColFg = canvasMap[canvasRow][canvasCol][1] + canvasColText = canvasMap[canvasRow][canvasCol][2] + if colourLastBg != canvasColBg \ + or colourLastFg != canvasColFg: + colourLastBg = canvasColBg; colourLastFg = canvasColFg; + file.write("" + str(canvasColFg) + "," + str(canvasColBg)) + file.write(canvasColText) + file.write("\n") + return [True] + except IOError as error: + return [False, error] + # }}} # {{{ _updateStatusBar(): XXX def _updateStatusBar(self): text = "Foreground colour:" @@ -319,293 +458,99 @@ class MiRCARTFrame(wx.Frame): text += " " + str(self.panelCanvas.getBackgroundColour()) self.statusBar.SetStatusText(text) # }}} - # {{{ onAccelRedo(): XXX - def onAccelRedo(self, event): - self.panelCanvas.redo() - # }}} - # {{{ onAccelUndo(): XXX - def onAccelUndo(self, event): - self.panelCanvas.undo() - # }}} + # {{{ onCanvasUpdate(): XXX def onCanvasUpdate(self): if self.panelCanvas.patchesUndo[self.panelCanvas.patchesUndoLevel] != None: - self.menuEditUndo.Enable(True) + self.menuItemsById[self.CID_UNDO[0]].Enable(True) else: - self.menuEditUndo.Enable(False) + self.menuItemsById[self.CID_UNDO[0]].Enable(False) if self.panelCanvas.patchesUndoLevel > 0: - self.menuEditRedo.Enable(True) + self.menuItemsById[self.CID_REDO[0]].Enable(True) else: - self.menuEditRedo.Enable(False) + self.menuItemsById[self.CID_REDO[0]].Enable(False) # }}} - # {{{ onEditCopy(): XXX - def onEditCopy(self, event): - pass - # }}} - # {{{ onEditCut(): XXX - def onEditCut(self, event): - pass - # }}} - # {{{ onEditDecrBrush(): XXX - def onEditDecrBrush(self, event): - pass - # }}} - # {{{ onEditDelete(): XXX - def onEditDelete(self, event): - pass - # }}} - # {{{ onEditIncrBrush(): XXX - def onEditIncrBrush(self, event): - pass - # }}} - # {{{ onEditPaste(): XXX - def onEditPaste(self, event): - pass - # }}} - # {{{ onEditRedo(): XXX - def onEditRedo(self, event): - self.panelCanvas.redo() - # }}} - # {{{ onEditSolidBrush(): XXX - def onEditSolidBrush(self, event): - pass - # }}} - # {{{ onEditUndo(): XXX - def onEditUndo(self, event): - self.panelCanvas.undo() - # }}} - # {{{ onFileExit(): XXX - def onFileExit(self, event): - self.Close(True) - # }}} - # {{{ onFileExportPastebin(): XXX - def onFileExportPastebin(self, event): - pass - # }}} - # {{{ onFileExportPng(): XXX - def onFileExportPng(self, event): - pass - # }}} - # {{{ onFileNew(): XXX - def onFileNew(self, event): - pass - # }}} - # {{{ onFileOpen(): XXX - def onFileOpen(self, event): - pass - # }}} - # {{{ onFileSave(): XXX - def onFileSave(self, event): - pass - # }}} - # {{{ onFileSaveAs(): XXX - def onFileSaveAs(self, event): - with wx.FileDialog(self, "Save As...", os.getcwd(), "", \ - "*.txt", wx.FD_SAVE|wx.FD_OVERWRITE_PROMPT) as dialog: - if dialog.ShowModal() == wx.ID_CANCEL: - return - else: - try: - with open(dialog.GetPath(), "w") as file: - canvasMap = self.panelCanvas.getMap() - canvasHeight = self.panelCanvas.getHeight() - canvasWidth = self.panelCanvas.getWidth() - for canvasRow in range(0, canvasHeight): - colourLastBg = colourLastFg = None; - for canvasCol in range(0, canvasWidth): - canvasColBg = canvasMap[canvasRow][canvasCol][0] - canvasColFg = canvasMap[canvasRow][canvasCol][1] - canvasColText = canvasMap[canvasRow][canvasCol][2] - if colourLastBg != canvasColBg \ - or colourLastFg != canvasColFg: - colourLastBg = canvasColBg; colourLastFg = canvasColFg; - file.write("" + str(canvasColFg) + "," + str(canvasColBg)) - file.write(canvasColText) - file.write("\n") - except IOError as error: - wx.LogError("IOError {}".format(error)) - # }}} - # {{{ onPaletteEvent(): XXX - def onPaletteEvent(self, leftDown, rightDown, numColour): - self.panelCanvas.onPaletteEvent(leftDown, rightDown, numColour) - self._updateStatusBar() - # }}} - # {{{ onToolColourBg(): XXX - def onToolColourBg(self, event): - itemId = event.GetId() - for numColour in range(0, len(mircColours)): - if self.toolBarIdColours[numColour] == itemId: - self.onPaletteEvent(False, True, numColour) - # }}} - # {{{ onToolColourFg(): XXX - def onToolColourFg(self, event): - itemId = event.GetId() - for numColour in range(0, len(mircColours)): - if self.toolBarIdColours[numColour] == itemId: - self.onPaletteEvent(True, False, numColour) - # }}} - # {{{ onToolsRect(): XXX - def onToolsRect(self, event): - pass - # }}} - # {{{ onToolsCircle(): XXX - def onToolsCircle(self, event): - pass - # }}} - # {{{ onToolsLine(): XXX - def onToolsLine(self, event): - pass + # {{{ onFrameCommand(): XXX + def onFrameCommand(self, event): + cid = event.GetId() + if cid == self.CID_NEW[0]: + pass + elif cid == self.CID_OPEN[0]: + pass + elif cid == self.CID_SAVE[0]: + pass + elif cid == self.CID_SAVEAS[0]: + with wx.FileDialog(self, self.CID_SAVEAS[2], os.getcwd(), "", \ + "*.txt", wx.FD_SAVE|wx.FD_OVERWRITE_PROMPT) as dialog: + if dialog.ShowModal() == wx.ID_CANCEL: + return + else: + self._saveAs(dialog.GetPath()) + elif cid == self.CID_EXPORT_PASTEBIN[0]: + pass + elif cid == self.CID_EXPORT_AS_PNG[0]: + pass + elif cid == self.CID_EXIT[0]: + self.Close(True) + elif cid == self.CID_UNDO[0]: + self.panelCanvas.undo() + elif cid == self.CID_REDO[0]: + self.panelCanvas.redo() + elif cid == self.CID_CUT[0]: + pass + elif cid == self.CID_COPY[0]: + pass + elif cid == self.CID_PASTE[0]: + pass + elif cid == self.CID_DELETE[0]: + pass + elif cid == self.CID_INCRBRUSH[0]: + pass + elif cid == self.CID_DECRBRUSH[0]: + pass + elif cid == self.CID_SOLIDBRUSH[0]: + pass + elif cid == self.CID_RECT[0]: + pass + elif cid == self.CID_CIRCLE[0]: + pass + elif cid == self.CID_LINE[0]: + pass + elif cid >= self.CID_COLOUR00[0] \ + and cid <= self.CID_COLOUR15[0]: + if event.GetEventType() == wx.wxEVT_TOOL: + leftIsDown = True; rightIsDown = False; + elif event.GetEventType() == wx.wxEVT_TOOL_RCLICKED: + leftIsDown = False; rightIsDown = True; + numColour = cid - self.CID_COLOUR00[0] + self.panelCanvas.onPaletteEvent(leftIsDown, rightIsDown, numColour) + self._updateStatusBar() # }}} # {{{ Initialisation method def __init__(self, parent, appSize=(800, 600), canvasPos=(25, 50), cellSize=(7, 14), canvasSize=(80, 25)): super().__init__(parent, wx.ID_ANY, "MiRCART", size=appSize) - self.panelSkin = wx.Panel(self, wx.ID_ANY) - self.panelCanvas = MiRCARTCanvas(self.panelSkin, \ - parentFrame=self, canvasPos=canvasPos, cellSize=cellSize, \ + self.panelCanvas = MiRCARTCanvas(self.panelSkin, \ + parentFrame=self, canvasPos=canvasPos, cellSize=cellSize, \ canvasSize=canvasSize, canvasTools=[MiRCARTToolRect]) - self.menuFile = wx.Menu() - self.menuFileNew = self.menuFile.Append(wx.ID_NEW, "&New", "New") - self.Bind(wx.EVT_MENU, self.onFileNew, self.menuFileNew) - self.menuFileOpen = self.menuFile.Append(wx.ID_OPEN, "&Open...", "Open...") - self.Bind(wx.EVT_MENU, self.onFileOpen, self.menuFileOpen) - self.menuFileSave = self.menuFile.Append(wx.ID_SAVE, "&Save", "Save") - self.Bind(wx.EVT_MENU, self.onFileSave, self.menuFileSave) - self.menuFileSaveAs = self.menuFile.Append(wx.ID_SAVEAS, "Save &As...", "Save As...") - self.Bind(wx.EVT_MENU, self.onFileSaveAs, self.menuFileSaveAs) - self.menuFile.AppendSeparator() - self.menuFileExportPastebin = self.menuFile.Append(wx.NewId(), "Export to Pasteb&in...", "Export to Pastebin...") - self.Bind(wx.EVT_MENU, self.onFileExportPastebin, self.menuFileExportPastebin) - self.menuFileExportPng = self.menuFile.Append(wx.NewId(), "Export as PN&G...", "Export as PNG...") - self.Bind(wx.EVT_MENU, self.onFileExportPng, self.menuFileExportPng) - self.menuFile.AppendSeparator() - self.menuFileExit = self.menuFile.Append(wx.ID_EXIT, "E&xit", "Exit") - self.Bind(wx.EVT_MENU, self.onFileExit, self.menuFileExit) - - self.menuEdit = wx.Menu() - self.menuEditUndo = self.menuEdit.Append(wx.ID_UNDO, "&Undo", "Undo") - self.menuEditUndo.Enable(False) - self.Bind(wx.EVT_MENU, self.onEditUndo, self.menuEditUndo) - self.menuEditRedo = self.menuEdit.Append(wx.ID_REDO, "&Redo", "Redo") - self.menuEditRedo.Enable(False) - self.Bind(wx.EVT_MENU, self.onEditRedo, self.menuEditRedo) - self.menuEdit.AppendSeparator() - self.menuEditCut = self.menuEdit.Append(wx.ID_CUT, "Cu&t", "Cut") - self.Bind(wx.EVT_MENU, self.onEditCut, self.menuEditCut) - self.menuEditCopy = self.menuEdit.Append(wx.ID_COPY, "&Copy", "Copy") - self.Bind(wx.EVT_MENU, self.onEditCopy, self.menuEditCopy) - self.menuEditPaste = self.menuEdit.Append(wx.ID_PASTE, "&Paste", "Paste") - self.Bind(wx.EVT_MENU, self.onEditPaste, self.menuEditPaste) - self.menuEditDelete = self.menuEdit.Append(wx.ID_DELETE, "De&lete", "Delete") - self.Bind(wx.EVT_MENU, self.onEditDelete, self.menuEditDelete) - self.menuEdit.AppendSeparator() - self.menuEditIncrBrush = self.menuEdit.Append(wx.NewId(), "&Increase brush size", "Increase brush size") - self.Bind(wx.EVT_MENU, self.onEditIncrBrush, self.menuEditIncrBrush) - self.menuEditDecrBrush = self.menuEdit.Append(wx.NewId(), "&Decrease brush size", "Decrease brush size") - self.Bind(wx.EVT_MENU, self.onEditDecrBrush, self.menuEditDecrBrush) - self.menuEditSolidBrush = self.menuEdit.AppendRadioItem(wx.NewId(), "&Solid brush", "Solid brush") - self.Bind(wx.EVT_MENU, self.onEditSolidBrush, self.menuEditSolidBrush) - - self.menuTools = wx.Menu() - self.menuToolsRect = self.menuTools.AppendRadioItem(wx.NewId(), "&Rectangle", "Rectangle") - self.Bind(wx.EVT_MENU, self.onToolsRect, self.menuToolsRect) - self.menuToolsCircle = self.menuTools.AppendRadioItem(wx.NewId(), "&Circle", "Circle") - self.Bind(wx.EVT_MENU, self.onToolsCircle, self.menuToolsCircle) - self.menuToolsLine = self.menuTools.AppendRadioItem(wx.NewId(), "&Line", "Line") - self.Bind(wx.EVT_MENU, self.onToolsLine, self.menuToolsLine) - - self.menuBar = wx.MenuBar() - self.menuBar.Append(self.menuFile, "&File") - self.menuBar.Append(self.menuEdit, "&Edit") - self.menuBar.Append(self.menuTools, "&Tools") + self.menuItemsById = {}; self.menuBar = wx.MenuBar(); + self._initMenus(self.menuBar, \ + [self.MID_FILE, self.MID_EDIT, self.MID_TOOLS], self.onFrameCommand) self.SetMenuBar(self.menuBar) - accelTableEntries = [wx.AcceleratorEntry() for n in range(2)] - self.accelRedoId = wx.NewId() - accelTableEntries[0].Set(wx.ACCEL_CTRL, ord('Y'), self.accelRedoId) - self.Bind(wx.EVT_MENU, self.onAccelRedo, id=self.accelRedoId) - self.accelUndoId = wx.NewId() - accelTableEntries[1].Set(wx.ACCEL_CTRL, ord('Z'), self.accelUndoId) - self.Bind(wx.EVT_MENU, self.onAccelUndo, id=self.accelUndoId) - self.accelTable = wx.AcceleratorTable(accelTableEntries) - self.SetAcceleratorTable(self.accelTable) - - self.statusBar = self.CreateStatusBar() - self._updateStatusBar() - - self.toolBar = wx.ToolBar(self.panelSkin, -1, style=wx.HORIZONTAL|wx.TB_FLAT|wx.TB_NODIVIDER) + self.toolBar = wx.ToolBar(self.panelSkin, -1, \ + style=wx.HORIZONTAL|wx.TB_FLAT|wx.TB_NODIVIDER) self.toolBar.SetToolBitmapSize((16,16)) - self.toolNew = self.toolBar.AddTool(wx.NewId(), "New", \ - wx.ArtProvider.GetBitmap(wx.ART_NEW, wx.ART_TOOLBAR, (16,16))) - self.Bind(wx.EVT_TOOL, self.onFileNew, self.toolNew) - self.toolOpen = self.toolBar.AddTool(wx.NewId(), "Open", \ - wx.ArtProvider.GetBitmap(wx.ART_FILE_OPEN, wx.ART_TOOLBAR, (16,16))) - self.Bind(wx.EVT_TOOL, self.onFileOpen, self.toolOpen) - self.toolSave = self.toolBar.AddTool(wx.NewId(), "Save", \ - wx.ArtProvider.GetBitmap(wx.ART_FILE_SAVE, wx.ART_TOOLBAR, (16,16))) - self.Bind(wx.EVT_TOOL, self.onFileSave, self.toolSave) - self.toolSaveAs = self.toolBar.AddTool(wx.NewId(), "Save As...", \ - wx.ArtProvider.GetBitmap(wx.ART_FILE_SAVE_AS, wx.ART_TOOLBAR, (16,16))) - self.Bind(wx.EVT_TOOL, self.onFileSaveAs, self.toolSaveAs) - self.toolUndo = self.toolBar.AddTool(wx.NewId(), "Undo", \ - wx.ArtProvider.GetBitmap(wx.ART_UNDO, wx.ART_TOOLBAR, (16,16))) - self.Bind(wx.EVT_TOOL, self.onEditUndo, self.toolUndo) - self.toolRedo = self.toolBar.AddTool(wx.NewId(), "Redo", \ - wx.ArtProvider.GetBitmap(wx.ART_REDO, wx.ART_TOOLBAR, (16,16))) - self.Bind(wx.EVT_TOOL, self.onEditRedo, self.toolRedo) - self.toolBar.AddSeparator() - self.toolCut = self.toolBar.AddTool(wx.NewId(), "Cut", \ - wx.ArtProvider.GetBitmap(wx.ART_CUT, wx.ART_TOOLBAR, (16,16))) - self.Bind(wx.EVT_TOOL, self.onEditCut, self.toolCut) - self.toolCopy = self.toolBar.AddTool(wx.NewId(), "Copy", \ - wx.ArtProvider.GetBitmap(wx.ART_COPY, wx.ART_TOOLBAR, (16,16))) - self.Bind(wx.EVT_TOOL, self.onEditCopy, self.toolCopy) - self.toolPaste = self.toolBar.AddTool(wx.NewId(), "Paste", \ - wx.ArtProvider.GetBitmap(wx.ART_PASTE, wx.ART_TOOLBAR, (16,16))) - self.Bind(wx.EVT_TOOL, self.onEditPaste, self.toolPaste) - self.toolDelete = self.toolBar.AddTool(wx.NewId(), "Delete", \ - wx.ArtProvider.GetBitmap(wx.ART_DELETE, wx.ART_TOOLBAR, (16,16))) - self.Bind(wx.EVT_TOOL, self.onEditDelete, self.toolDelete) - self.toolBar.AddSeparator() - self.toolIncrBrush = self.toolBar.AddTool(wx.NewId(), "Increase brush size", \ - wx.ArtProvider.GetBitmap(wx.ART_PLUS, wx.ART_TOOLBAR, (16,16))) - self.Bind(wx.EVT_TOOL, self.onEditIncrBrush, self.toolIncrBrush) - self.toolDecrBrush = self.toolBar.AddTool(wx.NewId(), "Decrease brush size", \ - wx.ArtProvider.GetBitmap(wx.ART_MINUS, wx.ART_TOOLBAR, (16,16))) - self.Bind(wx.EVT_TOOL, self.onEditDecrBrush, self.toolDecrBrush) - self.toolSolidBrush = self.toolBar.AddTool(wx.NewId(), "Solid brush", \ - wx.ArtProvider.GetBitmap(wx.ART_HELP, wx.ART_TOOLBAR, (16,16))) - self.Bind(wx.EVT_TOOL, self.onEditSolidBrush, self.toolSolidBrush) - self.toolBar.AddSeparator() - self.toolRect = self.toolBar.AddTool(wx.NewId(), "Rectangle", \ - wx.ArtProvider.GetBitmap(wx.ART_HELP, wx.ART_TOOLBAR, (16,16))) - self.Bind(wx.EVT_TOOL, self.onToolsRect, self.toolRect) - self.toolCircle = self.toolBar.AddTool(wx.NewId(), "Circle", \ - wx.ArtProvider.GetBitmap(wx.ART_HELP, wx.ART_TOOLBAR, (16,16))) - self.Bind(wx.EVT_TOOL, self.onToolsCircle, self.toolCircle) - self.toolLine = self.toolBar.AddTool(wx.NewId(), "Line", \ - wx.ArtProvider.GetBitmap(wx.ART_HELP, wx.ART_TOOLBAR, (16,16))) - self.Bind(wx.EVT_TOOL, self.onToolsLine, self.toolLine) - self.toolBar.AddSeparator() - self.toolBarIdColours = [None for x in range(0, len(mircColours))] - self.toolBarBitmapColours = [None for x in range(0, len(mircColours))] - for numColour in range(0, len(mircColours)): - self.toolBarBitmapColours[numColour] = wx.Bitmap((16,16)) - tmpDc = wx.MemoryDC(); tmpDc.SelectObject(self.toolBarBitmapColours[numColour]); - tmpDc.SetBrush(self.panelCanvas.mircBrushes[numColour]) - tmpDc.SetBackground(self.panelCanvas.mircBrushes[numColour]) - tmpDc.SetPen(self.panelCanvas.mircPens[numColour]) - tmpDc.DrawRectangle(0, 0, 16, 16) - self.toolBarIdColours[numColour] = wx.NewId() - self.toolBar.AddTool(self.toolBarIdColours[numColour], \ - "mIRC colour #" + str(numColour), self.toolBarBitmapColours[numColour]) - self.Bind(wx.EVT_TOOL, self.onToolColourFg, id=self.toolBarIdColours[numColour]) - self.Bind(wx.EVT_TOOL_RCLICKED, self.onToolColourBg, id=self.toolBarIdColours[numColour]) + self._initToolBars(self.toolBar, [self.BID_TOOLBAR], self.onFrameCommand) self.toolBar.Realize(); self.toolBar.Fit(); - self.SetFocus() - self.Show(True) + self.accelTable = wx.AcceleratorTable( \ + self._initAccelTable(self.AID_EDIT, self.onFrameCommand)) + self.SetAcceleratorTable(self.accelTable) + + self.statusBar = self.CreateStatusBar(); self._updateStatusBar(); + self.SetFocus(); self.Show(True); self.onCanvasUpdate(); # }}} # From 44e593714d9c048a03adc044e16029f278803ac7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Sat, 6 Jan 2018 01:44:45 +0100 Subject: [PATCH 066/148] {IrcClient,IrcMiRCARTBot.py,MiRC{2png,ART}}.py: include parameters in function header comments. --- IrcClient.py | 12 ++++---- IrcMiRCARTBot.py | 32 ++++++++++----------- MiRC2png.py | 20 ++++++------- MiRCART.py | 75 ++++++++++++++++++++++++------------------------ 4 files changed, 69 insertions(+), 70 deletions(-) diff --git a/IrcClient.py b/IrcClient.py index 61e6d31..dc407be 100644 --- a/IrcClient.py +++ b/IrcClient.py @@ -32,13 +32,13 @@ class IrcClient: clientSocket = clientSocketFile = None; clientNextTimeout = None; clientQueue = None; - # {{{ close(): Close connection to server + # {{{ close(self): Close connection to server def close(self): if self.clientSocket != None: self.clientSocket.close() self.clientSocket = self.clientSocketFile = None; # }}} - # {{{ connect(): Connect to server and register w/ optional timeout + # {{{ connect(self, timeout=None): Connect to server and register w/ optional timeout def connect(self, timeout=None): self.clientSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.clientSocket.setblocking(0) @@ -58,7 +58,7 @@ class IrcClient: self.queue("USER", self.clientIdent, "0", "0", self.clientGecos) return True # }}} - # {{{ readline(): Read and parse single line from server into canonicalised list, honouring timers + # {{{ readline(self): Read and parse single line from server into canonicalised list, honouring timers def readline(self): if self.clientNextTimeout: timeNow = time.time() @@ -87,7 +87,7 @@ class IrcClient: msg = [""] + msg[0:] return msg # }}} - # {{{ queue(): Parse and queue single line to server from list + # {{{ queue(self, *args): Parse and queue single line to server from list def queue(self, *args): msg = ""; argNumMax = len(args); for argNum in range(0, argNumMax): @@ -97,7 +97,7 @@ class IrcClient: msg += args[argNum] + " " self.clientQueue.append((msg + "\r\n").encode()) # }}} - # {{{ unqueue(): Send all queued lines to server, honouring timers + # {{{ unqueue(self): Send all queued lines to server, honouring timers def unqueue(self): while self.clientQueue: msg = self.clientQueue[0]; msgLen = len(msg); msgBytesSent = 0; @@ -118,7 +118,7 @@ class IrcClient: # }}} # - # Initialisation method + # __init__(self, serverHname, serverPort, clientNick, clientIdent, clientGecos): initialisation method def __init__(self, serverHname, serverPort, clientNick, clientIdent, clientGecos): self.serverHname = serverHname; self.serverPort = serverPort; self.clientNick = clientNick; self.clientIdent = clientIdent; self.clientGecos = clientGecos; diff --git a/IrcMiRCARTBot.py b/IrcMiRCARTBot.py index 7932e3d..f996f49 100755 --- a/IrcMiRCARTBot.py +++ b/IrcMiRCARTBot.py @@ -33,17 +33,17 @@ class IrcMiRCARTBot(IrcClient.IrcClient): clientChannelLastMessage = clientChannelOps = clientChannel = None clientChannelRejoin = None - # {{{ ContentTooLargeException: Raised by _urlretrieveReportHook() given download size > 1 MB + # {{{ ContentTooLargeException(Exception): Raised by _urlretrieveReportHook() given download size > 1 MB class ContentTooLargeException(Exception): pass # }}} - # {{{ _dispatch001(): Dispatch single 001 (RPL_WELCOME) + # {{{ _dispatch001(self, message): Dispatch single 001 (RPL_WELCOME) def _dispatch001(self, message): self._log("Registered on {}:{} as {}, {}, {}.".format(self.serverHname, self.serverPort, self.clientNick, self.clientIdent, self.clientGecos)) self._log("Attempting to join {} on {}:{}...".format(self.clientChannel, self.serverHname, self.serverPort)) self.queue("JOIN", self.clientChannel) # }}} - # {{{ _dispatch353(): Dispatch single 353 (RPL_NAMREPLY) + # {{{ _dispatch353(self, message): Dispatch single 353 (RPL_NAMREPLY) def _dispatch353(self, message): if message[4].lower() == self.clientChannel.lower(): for channelNickSpec in message[5].split(" "): @@ -53,19 +53,19 @@ class IrcMiRCARTBot(IrcClient.IrcClient): self.clientChannelOps.append(channelNickSpec[1:].lower()) self._log("Authorising {} on {}".format(channelNickSpec[1:].lower(), message[4].lower())) # }}} - # {{{ _dispatchJoin(): Dispatch single JOIN message from server + # {{{ _dispatchJoin(self, message): Dispatch single JOIN message from server def _dispatchJoin(self, message): self._log("Joined {} on {}:{}.".format(message[2].lower(), self.serverHname, self.serverPort)) self.clientNextTimeout = None; self.clientChannelRejoin = False; # }}} - # {{{ _dispatchKick(): Dispatch single KICK message from server + # {{{ _dispatchKick(self, message): Dispatch single KICK message from server def _dispatchKick(self, message): if message[2].lower() == self.clientChannel.lower() \ and message[3].lower() == self.clientNick.lower(): self._log("Kicked from {} by {}, rejoining in 15 seconds".format(message[2].lower(), message[0])) self.clientNextTimeout = time.time() + 15; self.clientChannelRejoin = True; # }}} - # {{{ _dispatchMode(): Dispatch single MODE message from server + # {{{ _dispatchMode(self, message): Dispatch single MODE message from server def _dispatchMode(self, message): if message[2].lower() == self.clientChannel.lower(): channelModeType = "+"; channelModeArg = 4; @@ -93,16 +93,16 @@ class IrcMiRCARTBot(IrcClient.IrcClient): self._log("Deauthorising {} on {}".format(channelAuthDel, message[2].lower())) self.clientChannelOps.remove(channelAuthDel) # }}} - # {{{ _dispatchNone(): Dispatch None message from server + # {{{ _dispatchNone(self): Dispatch None message from server def _dispatchNone(self): self._log("Disconnected from {}:{}.".format(self.serverHname, self.serverPort)) self.close() # }}} - # {{{ _dispatchPing(): Dispatch single PING message from server + # {{{ _dispatchPing(self, message): Dispatch single PING message from server def _dispatchPing(self, message): self.queue("PONG", message[2]) # }}} - # {{{ _dispatchPrivmsg(): Dispatch single PRIVMSG message from server + # {{{ _dispatchPrivmsg(self, message): Dispatch single PRIVMSG message from server def _dispatchPrivmsg(self, message): if message[2].lower() == self.clientChannel.lower() \ and message[3].startswith("!pngbot "): @@ -152,18 +152,18 @@ class IrcMiRCARTBot(IrcClient.IrcClient): if os.path.isfile(imgTmpFilePath): os.remove(imgTmpFilePath) # }}} - # {{{ _dispatchTimer(): Dispatch single client timer expiration + # {{{ _dispatchTimer(self): Dispatch single client timer expiration def _dispatchTimer(self): if self.clientChannelRejoin: self._log("Attempting to join {} on {}:{}...".format(self.clientChannel, self.serverHname, self.serverPort)) self.queue("JOIN", self.clientChannel) self.clientNextTimeout = time.time() + 15; self.clientChannelRejoin = True; # }}} - # {{{ _log(): Log single message to stdout w/ timestamp + # {{{ _log(self, msg): Log single message to stdout w/ timestamp def _log(self, msg): print(time.strftime("%Y/%m/%d %H:%M:%S") + " " + msg) # }}} - # {{{ _uploadToImgur(): Upload single file to Imgur + # {{{ _uploadToImgur(self, imgFilePath, imgName, imgTitle, apiKey): Upload single file to Imgur def _uploadToImgur(self, imgFilePath, imgName, imgTitle, apiKey): requestImageData = open(imgFilePath, "rb").read() requestData = { \ @@ -181,12 +181,12 @@ class IrcMiRCARTBot(IrcClient.IrcClient): else: return [responseHttp.status_code] # }}} - # {{{ _urlretrieveReportHook(): Limit downloads to 1 MB + # {{{ _urlretrieveReportHook(count, blockSize, totalSize): Limit downloads to 1 MB def _urlretrieveReportHook(count, blockSize, totalSize): if (totalSize > pow(2,20)): raise IrcMiRCARTBot.ContentTooLargeException # }}} - # {{{ connect(): Connect to server and (re)initialise w/ optional timeout + # {{{ connect(self, timeout=None): Connect to server and (re)initialise w/ optional timeout def connect(self, timeout=None): self._log("Connecting to {}:{}...".format(self.serverHname, self.serverPort)) if super().connect(timeout): @@ -198,7 +198,7 @@ class IrcMiRCARTBot(IrcClient.IrcClient): else: return False # }}} - # {{{ dispatch(): Read, parse, and dispatch single line from server + # {{{ dispatch(self): Read, parse, and dispatch single line from server def dispatch(self): while True: if self.clientNextTimeout: @@ -228,7 +228,7 @@ class IrcMiRCARTBot(IrcClient.IrcClient): # }}} # - # Initialisation method + # __init__(self, serverHname, serverPort="6667", clientNick="pngbot", clientIdent="pngbot", clientGecos="pngbot", clientChannel="#MiRCART"): initialisation method def __init__(self, serverHname, serverPort="6667", clientNick="pngbot", clientIdent="pngbot", clientGecos="pngbot", clientChannel="#MiRCART"): super().__init__(serverHname, serverPort, clientNick, clientIdent, clientGecos) self.clientChannel = clientChannel diff --git a/MiRC2png.py b/MiRC2png.py index 89c8976..7b1633e 100755 --- a/MiRC2png.py +++ b/MiRC2png.py @@ -81,7 +81,7 @@ class MiRC2png: (187, 187, 187, 255), # Light Grey ] # }}} - # {{{ _State: Parsing loop state + # {{{ _State(Enum): Parsing loop state class _State(Enum): STATE_CHAR = 1 STATE_COLOUR_SPEC = 2 @@ -89,15 +89,15 @@ class MiRC2png: STATE_CSPEC_DIGIT1 = 3 # }}} - # {{{ _countChar(): XXX + # {{{ _countChar(self, char): XXX def _countChar(self, char): return True # }}} - # {{{ _countColourSpecState(): XXX + # {{{ _countColourSpecState(self, colourSpec): XXX def _countColourSpecState(self, colourSpec): return 0 # }}} - # {{{ _render(): XXX + # {{{ _render(self): XXX def _render(self): self.outCurX = 0; self.outCurY = 0; for inCurRow in range(0, len(self.inLines)): @@ -116,7 +116,7 @@ class MiRC2png: self._syncColourSpecState) self.outCurX = 0; self.outCurY += self.outImgFontSize[1]; # }}} - # {{{ _getMaxCols(): Calculate widest row in lines, ignoring non-printable & mIRC control code sequences + # {{{ _getMaxCols(self, lines): Calculate widest row in lines, ignoring non-printable & mIRC control code sequences def _getMaxCols(self, lines): maxCols = 0; for inCurRow in range(0, len(lines)): @@ -136,7 +136,7 @@ class MiRC2png: maxCols = max(maxCols, curRowCols) return maxCols # }}} - # {{{ _parseAsChar(): Parse single character as regular character and mutate state + # {{{ _parseAsChar(self, char, fn): Parse single character as regular character and mutate state def _parseAsChar(self, char, fn): if char == "": self._State = self._State.STATE_CSPEC_DIGIT0; self.inCurCol += 1; @@ -144,7 +144,7 @@ class MiRC2png: else: self.inCurCol += 1; return fn(char); # }}} - # {{{ _parseAsColourSpec(): Parse single character as mIRC colour control code sequence and mutate state + # {{{ _parseAsColourSpec(self, char, fn): Parse single character as mIRC colour control code sequence and mutate state def _parseAsColourSpec(self, char, fn): if self._State == self._State.STATE_CSPEC_DIGIT0 \ and char == ",": @@ -170,7 +170,7 @@ class MiRC2png: self.inCurDigits = 0 return [True, result] # }}} - # {{{ _syncChar(): XXX + # {{{ _syncChar(self, char): XXX def _syncChar(self, char): if char == "": self.inCurBold = 0 if self.inCurBold else 1; @@ -207,7 +207,7 @@ class MiRC2png: self.outCurX += self.outImgFontSize[0]; return True # }}} - # {{{ _syncColourSpecState(): XXX + # {{{ _syncColourSpecState(self, colourSpec): XXX def _syncColourSpecState(self, colourSpec): if len(colourSpec) > 0: colourSpec = colourSpec.split(",") @@ -222,7 +222,7 @@ class MiRC2png: # }}} # - # Initialisation method + # __init__(self, inFilePath, imgFilePath, fontFilePath="DejaVuSansMono.ttf", fontSize=11): initialisation method def __init__(self, inFilePath, imgFilePath, fontFilePath="DejaVuSansMono.ttf", fontSize=11): self.inFilePath = inFilePath; self.inFile = open(inFilePath, "r"); self.inLines = self.inFile.readlines() diff --git a/MiRCART.py b/MiRCART.py index a8bd09f..f0de10f 100755 --- a/MiRCART.py +++ b/MiRCART.py @@ -55,7 +55,7 @@ class MiRCARTCanvas(wx.Panel): mircBg = mircFg = mircBrushes = mircPens = None patchesTmp = patchesUndo = patchesUndoLevel = None - # {{{ _drawPatch(): XXX + # {{{ _drawPatch(self, patch, eventDc, tmpDc, atX, atY): XXX def _drawPatch(self, patch, eventDc, tmpDc, atX, atY): patchXabs = (atX + patch[0]) * self.getCellWidth() patchYabs = (atY + patch[1]) * self.getCellHeight() @@ -67,17 +67,17 @@ class MiRCARTCanvas(wx.Panel): dc.DrawRectangle(patchXabs, patchYabs, \ self.getCellWidth(), self.getCellHeight()) # }}} - # {{{ _eventPointToMapX(): XXX + # {{{ _eventPointToMapX(self, eventPoint): XXX def _eventPointToMapX(self, eventPoint): rectX = eventPoint.x - (eventPoint.x % self.getCellWidth()) return int(rectX / self.getCellWidth() if rectX else 0) # }}} - # {{{ _eventPointToMapY(): XXX + # {{{ _eventPointToMapY(self, eventPoint): XXX def _eventPointToMapY(self, eventPoint): rectY = eventPoint.y - (eventPoint.y % self.getCellHeight()) return int(rectY / self.getCellHeight() if rectY else 0) # }}} - # {{{ _onMouseEvent(): XXX + # {{{ _onMouseEvent(self, event): XXX def _onMouseEvent(self, event): eventObject = event.GetEventObject() eventDc = wx.ClientDC(self); tmpDc = wx.MemoryDC(); @@ -92,7 +92,7 @@ class MiRCARTCanvas(wx.Panel): mapPatches = tool.onMouseDown(event, mapX, mapY, event.LeftIsDown(), event.RightIsDown()) self._processMapPatches(mapPatches, eventDc, tmpDc, mapX, mapY) # }}} - # {{{ _processMapPatches(): XXX + # {{{ _processMapPatches(self, mapPatches, eventDc, tmpDc, atX, atY): XXX def _processMapPatches(self, mapPatches, eventDc, tmpDc, atX, atY): for mapPatch in mapPatches: mapPatchTmp = mapPatch[0]; mapPatchW = mapPatch[1]; mapPatchH = mapPatch[2]; @@ -122,58 +122,58 @@ class MiRCARTCanvas(wx.Panel): if len(mapPatch[3]): self.parentFrame.onCanvasUpdate() # }}} - # {{{ getBackgroundColour(): XXX + # {{{ getBackgroundColour(self): XXX def getBackgroundColour(self): return self.mircBg # }}} - # {{{ getCellHeight(): XXX + # {{{ getCellHeight(self): XXX def getCellHeight(self): return self.cellSize[1] # }}} - # {{{ getCellWidth(): XXX + # {{{ getCellWidth(self): XXX def getCellWidth(self): return self.cellSize[0] # }}} - # {{{ getForegroundColour(): XXX + # {{{ getForegroundColour(self): XXX def getForegroundColour(self): return self.mircFg # }}} - # {{{ getHeight(): XXX + # {{{ getHeight(self): XXX def getHeight(self): return self.canvasSize[1] # }}} - # {{{ getMap(): XXX + # {{{ getMap(self): XXX def getMap(self): return self.canvasMap # }}} - # {{{ getWidth(): XXX + # {{{ getWidth(self): XXX def getWidth(self): return self.canvasSize[0] # }}} - # {{{ onLeftDown(): XXX + # {{{ onLeftDown(self, event): XXX def onLeftDown(self, event): self._onMouseEvent(event) # }}} - # {{{ onMotion(): XXX + # {{{ onMotion(self, event): XXX def onMotion(self, event): self._onMouseEvent(event) # }}} - # {{{ onPaint(): XXX + # {{{ onPaint(self, event): XXX def onPaint(self, event): eventDc = wx.BufferedPaintDC(self, self.canvasBitmap) # }}} - # {{{ onPaletteEvent(): XXX + # {{{ onPaletteEvent(self, leftDown, rightDown, numColour): XXX def onPaletteEvent(self, leftDown, rightDown, numColour): if leftDown: self.mircFg = numColour elif rightDown: self.mircBg = numColour # }}} - # {{{ onRightDown(): XXX + # {{{ onRightDown(self, event): XXX def onRightDown(self, event): self._onMouseEvent(event) # }}} - # {{{ redo(): XXX + # {{{ redo(self): XXX def redo(self): if self.patchesUndoLevel > 0: self.patchesUndoLevel -= 1 @@ -188,7 +188,7 @@ class MiRCARTCanvas(wx.Panel): else: return False # }}} - # {{{ undo(): XXX + # {{{ undo(self): XXX def undo(self): if self.patchesUndo[self.patchesUndoLevel] != None: undoPatch = self.patchesUndo[self.patchesUndoLevel][0] @@ -203,7 +203,7 @@ class MiRCARTCanvas(wx.Panel): else: return False # }}} - # {{{ Initialisation method + # {{{ __init__(self, parent, parentFrame, canvasPos, cellSize, canvasSize, canvasTools): Initialisation method def __init__(self, parent, parentFrame, canvasPos, cellSize, canvasSize, canvasTools): self.parentFrame = parentFrame canvasWinSize = (cellSize[0] * canvasSize[0], cellSize[1] * canvasSize[1]) @@ -239,15 +239,15 @@ class MiRCARTTool(): """XXX""" parentCanvas = None - # {{{ onMouseDown(): XXX + # {{{ onMouseDown(self, event, mapX, mapY, isLeftDown, isRightDown): XXX def onMouseDown(self, event, mapX, mapY, isLeftDown, isRightDown): pass # }}} - # {{{ onMouseMotion(): XXX + # {{{ onMouseMotion(self, event, mapX, mapY, isLeftDown, isRightDown): XXX def onMouseMotion(self, event, mapX, mapY, isLeftDown, isRightDown): pass # }}} - # {{{ Initialisation method + # {{{ __init__(self, parentCanvas): initialisation method def __init__(self, parentCanvas): self.parentCanvas = parentCanvas # }}} @@ -255,7 +255,7 @@ class MiRCARTTool(): class MiRCARTToolRect(MiRCARTTool): """XXX""" - # {{{ _draw(): XXX + # {{{ _draw(self, event, mapX, mapY, isLeftDown, isRightDown): XXX def _draw(self, event, mapX, mapY, isLeftDown, isRightDown): if isLeftDown: return [[False, 1, 1, [[0, 0, \ @@ -276,15 +276,15 @@ class MiRCARTToolRect(MiRCARTTool): self.parentCanvas.getForegroundColour(), \ self.parentCanvas.getForegroundColour(), " "]]]] # }}} - # {{{ onMouseDown(): XXX + # {{{ onMouseDown(self, event, mapX, mapY, isLeftDown, isRightDown): XXX def onMouseDown(self, event, mapX, mapY, isLeftDown, isRightDown): return self._draw(event, mapX, mapY, isLeftDown, isRightDown) # }}} - # {{{ onMouseMotion(): XXX + # {{{ onMouseMotion(self, event, mapX, mapY, isLeftDown, isRightDown): XXX def onMouseMotion(self, event, mapX, mapY, isLeftDown, isRightDown): return self._draw(event, mapX, mapY, isLeftDown, isRightDown) # }}} - # {{{ Initialisation method + # {{{ __init__(self, parentCanvas): initialisation method def __init__(self, parentCanvas): super().__init__(parentCanvas) # }}} @@ -292,8 +292,7 @@ class MiRCARTToolRect(MiRCARTTool): class MiRCARTFrame(wx.Frame): """XXX""" panelSkin = panelCanvas = None - menuItemsById = menuBar = toolBar = accelTable = None - statusBar = None + menuItemsById = menuBar = toolBar = accelTable = statusBar = None # {{{ Types TID_COMMAND = (0x001) @@ -372,7 +371,7 @@ class MiRCARTFrame(wx.Frame): AID_EDIT = (0x500, TID_ACCELS, (CID_UNDO, CID_REDO)) # }}} - # {{{ _drawIcon(): XXX + # {{{ _drawIcon(self, solidColour): XXX def _drawIcon(self, solidColour): iconBitmap = wx.Bitmap((16,16)) iconDc = wx.MemoryDC(); iconDc.SelectObject(iconBitmap); @@ -382,7 +381,7 @@ class MiRCARTFrame(wx.Frame): iconDc.DrawRectangle(0, 0, 16, 16) return iconBitmap # }}} - # {{{ _initAccelTable(): XXX + # {{{ _initAccelTable(self, accelsDescr, handler): XXX def _initAccelTable(self, accelsDescr, handler): accelTableEntries = [wx.AcceleratorEntry() for n in range(0, len(accelsDescr[2]))] for numAccel in range(0, len(accelsDescr[2])): @@ -392,7 +391,7 @@ class MiRCARTFrame(wx.Frame): self.Bind(wx.EVT_MENU, handler, id=accelDescr[0]) return accelTableEntries # }}} - # {{{ _initMenus(): XXX + # {{{ _initMenus(self, menuBar, menusDescr, handler): XXX def _initMenus(self, menuBar, menusDescr, handler): for menuDescr in menusDescr: menuWindow = wx.Menu() @@ -405,7 +404,7 @@ class MiRCARTFrame(wx.Frame): self.Bind(wx.EVT_MENU, handler, menuItemWindow) menuBar.Append(menuWindow, menuDescr[3]) # }}} - # {{{ _initToolBars(): XXX + # {{{ _initToolBars(self, toolBar, toolBarsDescr, handler): XXX def _initToolBars(self, toolBar, toolBarsDescr, handler): for toolBarDescr in toolBarsDescr: for toolBarItem in toolBarDescr[2]: @@ -426,7 +425,7 @@ class MiRCARTFrame(wx.Frame): self.Bind(wx.EVT_TOOL, handler, toolBarItemWindow) self.Bind(wx.EVT_TOOL_RCLICKED, handler, toolBarItemWindow) # }}} - # {{{ _saveAs(): XXX + # {{{ _saveAs(self, pathName): XXX def _saveAs(self, pathName): try: with open(pathName, "w") as file: @@ -449,7 +448,7 @@ class MiRCARTFrame(wx.Frame): except IOError as error: return [False, error] # }}} - # {{{ _updateStatusBar(): XXX + # {{{ _updateStatusBar(self): XXX def _updateStatusBar(self): text = "Foreground colour:" text += " " + str(self.panelCanvas.getForegroundColour()) @@ -459,7 +458,7 @@ class MiRCARTFrame(wx.Frame): self.statusBar.SetStatusText(text) # }}} - # {{{ onCanvasUpdate(): XXX + # {{{ onCanvasUpdate(self): XXX def onCanvasUpdate(self): if self.panelCanvas.patchesUndo[self.panelCanvas.patchesUndoLevel] != None: self.menuItemsById[self.CID_UNDO[0]].Enable(True) @@ -470,7 +469,7 @@ class MiRCARTFrame(wx.Frame): else: self.menuItemsById[self.CID_REDO[0]].Enable(False) # }}} - # {{{ onFrameCommand(): XXX + # {{{ onFrameCommand(self, event): XXX def onFrameCommand(self, event): cid = event.GetId() if cid == self.CID_NEW[0]: @@ -526,7 +525,7 @@ class MiRCARTFrame(wx.Frame): self.panelCanvas.onPaletteEvent(leftIsDown, rightIsDown, numColour) self._updateStatusBar() # }}} - # {{{ Initialisation method + # {{{ __init__(self, parent, appSize=(800, 600), canvasPos=(25, 50), cellSize=(7, 14), canvasSize=(80, 25)): initialisation method def __init__(self, parent, appSize=(800, 600), canvasPos=(25, 50), cellSize=(7, 14), canvasSize=(80, 25)): super().__init__(parent, wx.ID_ANY, "MiRCART", size=appSize) self.panelSkin = wx.Panel(self, wx.ID_ANY) From 16065401e8738e65ac53bb34c1a6d3d9f19f1685 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Sat, 6 Jan 2018 02:42:26 +0100 Subject: [PATCH 067/148] MiRCART.py:MiRCARTCanvas: cleaned up. --- MiRCART.py | 258 +++++++++++++++++++++++------------------------------ 1 file changed, 112 insertions(+), 146 deletions(-) diff --git a/MiRCART.py b/MiRCART.py index f0de10f..d93344a 100755 --- a/MiRCART.py +++ b/MiRCART.py @@ -55,134 +55,101 @@ class MiRCARTCanvas(wx.Panel): mircBg = mircFg = mircBrushes = mircPens = None patchesTmp = patchesUndo = patchesUndoLevel = None - # {{{ _drawPatch(self, patch, eventDc, tmpDc, atX, atY): XXX - def _drawPatch(self, patch, eventDc, tmpDc, atX, atY): - patchXabs = (atX + patch[0]) * self.getCellWidth() - patchYabs = (atY + patch[1]) * self.getCellHeight() - brushFg = self.mircBrushes[patch[2]] - brushBg = self.mircBrushes[patch[3]] + # {{{ _drawPatch(self, patch, eventDc, tmpDc, atPoint): XXX + def _drawPatch(self, patch, eventDc, tmpDc, atPoint): + absPoint = self._relMapPointToAbsPoint((patch[0], patch[1]), atPoint) + brushFg = self.mircBrushes[patch[2]]; brushBg = self.mircBrushes[patch[3]]; pen = self.mircPens[patch[2]] for dc in (eventDc, tmpDc): dc.SetBrush(brushFg); dc.SetBackground(brushBg); dc.SetPen(pen); - dc.DrawRectangle(patchXabs, patchYabs, \ - self.getCellWidth(), self.getCellHeight()) + dc.DrawRectangle(absPoint[0], absPoint[1], self.cellSize[0], self.cellSize[1]) # }}} - # {{{ _eventPointToMapX(self, eventPoint): XXX - def _eventPointToMapX(self, eventPoint): - rectX = eventPoint.x - (eventPoint.x % self.getCellWidth()) - return int(rectX / self.getCellWidth() if rectX else 0) + # {{{ _eventPointToMapPoint(self, eventPoint): XXX + def _eventPointToMapPoint(self, eventPoint): + rectX = eventPoint.x - (eventPoint.x % self.cellSize[0]) + rectY = eventPoint.y - (eventPoint.y % self.cellSize[1]) + mapX = int(rectX / self.cellSize[0] if rectX else 0) + mapY = int(rectY / self.cellSize[1] if rectY else 0) + return (mapX, mapY) # }}} - # {{{ _eventPointToMapY(self, eventPoint): XXX - def _eventPointToMapY(self, eventPoint): - rectY = eventPoint.y - (eventPoint.y % self.getCellHeight()) - return int(rectY / self.getCellHeight() if rectY else 0) + # {{{ _getMapCell(self, absMapPoint): XXX + def _getMapCell(self, absMapPoint): + return self.canvasMap[absMapPoint[1]][absMapPoint[0]] # }}} - # {{{ _onMouseEvent(self, event): XXX - def _onMouseEvent(self, event): + # {{{ _processMapPatches(self, mapPatches, eventDc, tmpDc, atPoint): XXX + def _processMapPatches(self, mapPatches, eventDc, tmpDc, atPoint): + for mapPatch in mapPatches: + mapPatchTmp = mapPatch[0] + if mapPatchTmp and self.patchesTmp: + for patch in self.patchesTmp: + patch[2:] = self._getMapCell([patch[0], patch[1]]) + self._drawPatch(patch, eventDc, tmpDc, (0, 0)) + self.patchesTmp = [] + for patch in mapPatch[1]: + absMapPoint = self._relMapPointToAbsMapPoint(patch[0:2], atPoint) + mapItem = self._getMapCell(absMapPoint) + if mapPatchTmp: + self.patchesTmp.append([*absMapPoint, None, None, None]) + self._drawPatch(patch, eventDc, tmpDc, atPoint) + elif mapItem != patch[2:5]: + self._pushUndo(atPoint, patch, mapItem) + self._setMapCell(absMapPoint, *patch[2:5]) + self._drawPatch(patch, eventDc, tmpDc, atPoint) + self.parentFrame.onCanvasUpdate() + # }}} + # {{{ _pushUndo(self, atPoint, patch): XXXX + def _pushUndo(self, atPoint, patch, mapItem): + if self.patchesUndoLevel > 0: + del self.patchesUndo[0:self.patchesUndoLevel] + self.patchesUndoLevel = 0 + absMapPoint = self._relMapPointToAbsMapPoint((patch[0], patch[1]), atPoint) + self.patchesUndo.insert(0, ( \ + (absMapPoint[0], absMapPoint[1], mapItem[0], mapItem[1], mapItem[2]), \ + (absMapPoint[0], absMapPoint[1], patch[2], patch[3], patch[4]))) + # }}} + # {{{ _relMapPointToAbsMapPoint(self, relMapPoint, atPoint): XXX + def _relMapPointToAbsMapPoint(self, relMapPoint, atPoint): + return (atPoint[0] + relMapPoint[0], atPoint[1] + relMapPoint[1]) + # }}} + # {{{ _relMapPointToAbsPoint(self, relMapPoint, atPoint): XXX + def _relMapPointToAbsPoint(self, relMapPoint, atPoint): + absX = (atPoint[0] + relMapPoint[0]) * self.cellSize[0] + absY = (atPoint[1] + relMapPoint[1]) * self.cellSize[1] + return (absX, absY) + # }}} + # {{{ _setMapCell(self, absMapPoint, colourFg, colourBg, char): XXX + def _setMapCell(self, absMapPoint, colourFg, colourBg, char): + self.canvasMap[absMapPoint[1]][absMapPoint[0]] = [colourFg, colourBg, char] + # }}} + + # {{{ onMouseEvent(self, event): XXX + def onMouseEvent(self, event): eventObject = event.GetEventObject() eventDc = wx.ClientDC(self); tmpDc = wx.MemoryDC(); tmpDc.SelectObject(self.canvasBitmap) eventPoint = event.GetLogicalPosition(eventDc) - mapX = self._eventPointToMapX(eventPoint) - mapY = self._eventPointToMapY(eventPoint) + mapPoint = self._eventPointToMapPoint(eventPoint) for tool in self.canvasTools: if event.Dragging(): - mapPatches = tool.onMouseMotion(event, mapX, mapY, event.LeftIsDown(), event.RightIsDown()) + mapPatches = tool.onMouseMotion(event, mapPoint, event.LeftIsDown(), event.RightIsDown()) else: - mapPatches = tool.onMouseDown(event, mapX, mapY, event.LeftIsDown(), event.RightIsDown()) - self._processMapPatches(mapPatches, eventDc, tmpDc, mapX, mapY) - # }}} - # {{{ _processMapPatches(self, mapPatches, eventDc, tmpDc, atX, atY): XXX - def _processMapPatches(self, mapPatches, eventDc, tmpDc, atX, atY): - for mapPatch in mapPatches: - mapPatchTmp = mapPatch[0]; mapPatchW = mapPatch[1]; mapPatchH = mapPatch[2]; - if mapPatchTmp and self.patchesTmp: - for patch in self.patchesTmp: - patch[2] = self.canvasMap[patch[1]][patch[0]][0] - patch[3] = self.canvasMap[patch[1]][patch[0]][1] - patch[4] = self.canvasMap[patch[1]][patch[0]][2] - self._drawPatch(patch, eventDc, tmpDc, 0, 0) - self.patchesTmp = [] - for patch in mapPatch[3]: - if mapPatchTmp: - mapItem = self.canvasMap[atY + patch[1]][atX + patch[0]] - self.patchesTmp.append([atX + patch[0], atY + patch[1], None, None, None]) - self._drawPatch(patch, eventDc, tmpDc, atX, atY) - else: - mapItem = self.canvasMap[atY + patch[1]][atX + patch[0]] - if mapItem != [patch[2], patch[3], patch[4]]: - if self.patchesUndoLevel > 0: - del self.patchesUndo[0:self.patchesUndoLevel] - self.patchesUndoLevel = 0 - self.patchesUndo.insert(0, ( \ - (atX + patch[0], atY + patch[1], mapItem[0], mapItem[1], mapItem[2]), \ - (atX + patch[0], atY + patch[1], patch[2], patch[3], " "))) - self.canvasMap[atY + patch[1]][atX + patch[0]] = [patch[2], patch[3], " "]; - self._drawPatch(patch, eventDc, tmpDc, atX, atY) - if len(mapPatch[3]): - self.parentFrame.onCanvasUpdate() - # }}} - # {{{ getBackgroundColour(self): XXX - def getBackgroundColour(self): - return self.mircBg - # }}} - # {{{ getCellHeight(self): XXX - def getCellHeight(self): - return self.cellSize[1] - # }}} - # {{{ getCellWidth(self): XXX - def getCellWidth(self): - return self.cellSize[0] - # }}} - # {{{ getForegroundColour(self): XXX - def getForegroundColour(self): - return self.mircFg - # }}} - # {{{ getHeight(self): XXX - def getHeight(self): - return self.canvasSize[1] - # }}} - # {{{ getMap(self): XXX - def getMap(self): - return self.canvasMap - # }}} - # {{{ getWidth(self): XXX - def getWidth(self): - return self.canvasSize[0] - # }}} - # {{{ onLeftDown(self, event): XXX - def onLeftDown(self, event): - self._onMouseEvent(event) - # }}} - # {{{ onMotion(self, event): XXX - def onMotion(self, event): - self._onMouseEvent(event) + mapPatches = tool.onMouseDown(event, mapPoint, event.LeftIsDown(), event.RightIsDown()) + self._processMapPatches(mapPatches, eventDc, tmpDc, mapPoint) # }}} # {{{ onPaint(self, event): XXX def onPaint(self, event): eventDc = wx.BufferedPaintDC(self, self.canvasBitmap) # }}} - # {{{ onPaletteEvent(self, leftDown, rightDown, numColour): XXX - def onPaletteEvent(self, leftDown, rightDown, numColour): - if leftDown: - self.mircFg = numColour - elif rightDown: - self.mircBg = numColour - # }}} - # {{{ onRightDown(self, event): XXX - def onRightDown(self, event): - self._onMouseEvent(event) - # }}} # {{{ redo(self): XXX def redo(self): if self.patchesUndoLevel > 0: self.patchesUndoLevel -= 1 redoPatch = self.patchesUndo[self.patchesUndoLevel][1] - self.canvasMap[redoPatch[1]][redoPatch[0]] = \ - [redoPatch[2], redoPatch[3], redoPatch[4]] + self._setMapCell([redoPatch[0], redoPatch[1]], \ + redoPatch[2], redoPatch[3], redoPatch[4]) eventDc = wx.ClientDC(self); tmpDc = wx.MemoryDC(); tmpDc.SelectObject(self.canvasBitmap) - self._drawPatch(redoPatch, eventDc, tmpDc, 0, 0) + self._drawPatch(redoPatch, eventDc, tmpDc, (0, 0)) self.parentFrame.onCanvasUpdate() return True else: @@ -192,11 +159,11 @@ class MiRCARTCanvas(wx.Panel): def undo(self): if self.patchesUndo[self.patchesUndoLevel] != None: undoPatch = self.patchesUndo[self.patchesUndoLevel][0] - self.canvasMap[undoPatch[1]][undoPatch[0]] = \ - [undoPatch[2], undoPatch[3], undoPatch[4]] + self._setMapCell([undoPatch[0], undoPatch[1]], \ + undoPatch[2], undoPatch[3], undoPatch[4]) eventDc = wx.ClientDC(self); tmpDc = wx.MemoryDC(); tmpDc.SelectObject(self.canvasBitmap) - self._drawPatch(undoPatch, eventDc, tmpDc, 0, 0) + self._drawPatch(undoPatch, eventDc, tmpDc, (0, 0)) self.patchesUndoLevel += 1 self.parentFrame.onCanvasUpdate() return True @@ -229,22 +196,22 @@ class MiRCARTCanvas(wx.Panel): self.patchesTmp = [] self.patchesUndo = [None]; self.patchesUndoLevel = 0; - self.Bind(wx.EVT_LEFT_DOWN, self.onLeftDown) - self.Bind(wx.EVT_MOTION, self.onMotion) + self.Bind(wx.EVT_LEFT_DOWN, self.onMouseEvent) + self.Bind(wx.EVT_MOTION, self.onMouseEvent) self.Bind(wx.EVT_PAINT, self.onPaint) - self.Bind(wx.EVT_RIGHT_DOWN, self.onRightDown) + self.Bind(wx.EVT_RIGHT_DOWN, self.onMouseEvent) # }}} class MiRCARTTool(): """XXX""" parentCanvas = None - # {{{ onMouseDown(self, event, mapX, mapY, isLeftDown, isRightDown): XXX - def onMouseDown(self, event, mapX, mapY, isLeftDown, isRightDown): + # {{{ onMouseDown(self, event, mapPoint, isLeftDown, isRightDown): XXX + def onMouseDown(self, event, mapPoint, isLeftDown, isRightDown): pass # }}} - # {{{ onMouseMotion(self, event, mapX, mapY, isLeftDown, isRightDown): XXX - def onMouseMotion(self, event, mapX, mapY, isLeftDown, isRightDown): + # {{{ onMouseMotion(self, event, mapPoint, isLeftDown, isRightDown): XXX + def onMouseMotion(self, event, mapPoint, isLeftDown, isRightDown): pass # }}} # {{{ __init__(self, parentCanvas): initialisation method @@ -255,34 +222,34 @@ class MiRCARTTool(): class MiRCARTToolRect(MiRCARTTool): """XXX""" - # {{{ _draw(self, event, mapX, mapY, isLeftDown, isRightDown): XXX - def _draw(self, event, mapX, mapY, isLeftDown, isRightDown): + # {{{ _draw(self, event, mapPoint, isLeftDown, isRightDown): XXX + def _draw(self, event, mapPoint, isLeftDown, isRightDown): if isLeftDown: - return [[False, 1, 1, [[0, 0, \ - self.parentCanvas.getForegroundColour(), \ - self.parentCanvas.getForegroundColour(), " "]]], - [True, 1, 1, [[0, 0, \ - self.parentCanvas.getForegroundColour(), \ - self.parentCanvas.getForegroundColour(), " "]]]] + return [[False, [[0, 0, \ + self.parentCanvas.mircFg, \ + self.parentCanvas.mircFg, " "]]], + [True, [[0, 0, \ + self.parentCanvas.mircFg, \ + self.parentCanvas.mircFg, " "]]]] elif isRightDown: - return [[False, 1, 1, [[0, 0, \ - self.parentCanvas.getBackgroundColour(), \ - self.parentCanvas.getBackgroundColour(), " "]]], \ - [True, 1, 1, [[0, 0, \ - self.parentCanvas.getBackgroundColour(), \ - self.parentCanvas.getBackgroundColour(), " "]]]] + return [[False, [[0, 0, \ + self.parentCanvas.mircBg, \ + self.parentCanvas.mircBg, " "]]], \ + [True, [[0, 0, \ + self.parentCanvas.mircBg, \ + self.parentCanvas.mircBg, " "]]]] else: - return [[True, 1, 1, [[0, 0, \ - self.parentCanvas.getForegroundColour(), \ - self.parentCanvas.getForegroundColour(), " "]]]] + return [[True, [[0, 0, \ + self.parentCanvas.mircFg, \ + self.parentCanvas.mircFg, " "]]]] # }}} - # {{{ onMouseDown(self, event, mapX, mapY, isLeftDown, isRightDown): XXX - def onMouseDown(self, event, mapX, mapY, isLeftDown, isRightDown): - return self._draw(event, mapX, mapY, isLeftDown, isRightDown) + # {{{ onMouseDown(self, event, mapPoint, isLeftDown, isRightDown): XXX + def onMouseDown(self, event, mapPoint, isLeftDown, isRightDown): + return self._draw(event, mapPoint, isLeftDown, isRightDown) # }}} - # {{{ onMouseMotion(self, event, mapX, mapY, isLeftDown, isRightDown): XXX - def onMouseMotion(self, event, mapX, mapY, isLeftDown, isRightDown): - return self._draw(event, mapX, mapY, isLeftDown, isRightDown) + # {{{ onMouseMotion(self, event, mapPoint, isLeftDown, isRightDown): XXX + def onMouseMotion(self, event, mapPoint, isLeftDown, isRightDown): + return self._draw(event, mapPoint, isLeftDown, isRightDown) # }}} # {{{ __init__(self, parentCanvas): initialisation method def __init__(self, parentCanvas): @@ -429,9 +396,9 @@ class MiRCARTFrame(wx.Frame): def _saveAs(self, pathName): try: with open(pathName, "w") as file: - canvasMap = self.panelCanvas.getMap() - canvasHeight = self.panelCanvas.getHeight() - canvasWidth = self.panelCanvas.getWidth() + canvasMap = self.panelCanvas.canvasMap + canvasHeight = self.panelCanvas.canvasSize[1] + canvasWidth = self.panelCanvas.canvasSize[0] for canvasRow in range(0, canvasHeight): colourLastBg = colourLastFg = None; for canvasCol in range(0, canvasWidth): @@ -451,10 +418,10 @@ class MiRCARTFrame(wx.Frame): # {{{ _updateStatusBar(self): XXX def _updateStatusBar(self): text = "Foreground colour:" - text += " " + str(self.panelCanvas.getForegroundColour()) + text += " " + str(self.panelCanvas.mircFg) text += " | " text += "Background colour:" - text += " " + str(self.panelCanvas.getBackgroundColour()) + text += " " + str(self.panelCanvas.mircBg) self.statusBar.SetStatusText(text) # }}} @@ -517,12 +484,11 @@ class MiRCARTFrame(wx.Frame): pass elif cid >= self.CID_COLOUR00[0] \ and cid <= self.CID_COLOUR15[0]: - if event.GetEventType() == wx.wxEVT_TOOL: - leftIsDown = True; rightIsDown = False; - elif event.GetEventType() == wx.wxEVT_TOOL_RCLICKED: - leftIsDown = False; rightIsDown = True; numColour = cid - self.CID_COLOUR00[0] - self.panelCanvas.onPaletteEvent(leftIsDown, rightIsDown, numColour) + if event.GetEventType() == wx.wxEVT_TOOL: + self.panelCanvas.mircFg = numColour + elif event.GetEventType() == wx.wxEVT_TOOL_RCLICKED: + self.panelCanvas.mircBg = numColour self._updateStatusBar() # }}} # {{{ __init__(self, parent, appSize=(800, 600), canvasPos=(25, 50), cellSize=(7, 14), canvasSize=(80, 25)): initialisation method From 968d07050e1b6f3fe133f53c21ea620e798bd82f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Sat, 6 Jan 2018 02:46:46 +0100 Subject: [PATCH 068/148] MiRCART.py:MiRCARTTool{,Rect}: cleaned up. --- MiRCART.py | 30 ++++++------------------------ 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/MiRCART.py b/MiRCART.py index d93344a..536113f 100755 --- a/MiRCART.py +++ b/MiRCART.py @@ -130,10 +130,8 @@ class MiRCARTCanvas(wx.Panel): eventPoint = event.GetLogicalPosition(eventDc) mapPoint = self._eventPointToMapPoint(eventPoint) for tool in self.canvasTools: - if event.Dragging(): - mapPatches = tool.onMouseMotion(event, mapPoint, event.LeftIsDown(), event.RightIsDown()) - else: - mapPatches = tool.onMouseDown(event, mapPoint, event.LeftIsDown(), event.RightIsDown()) + mapPatches = tool.onMouseEvent(event, mapPoint, event.Dragging(), \ + event.LeftIsDown(), event.RightIsDown()) self._processMapPatches(mapPatches, eventDc, tmpDc, mapPoint) # }}} # {{{ onPaint(self, event): XXX @@ -206,12 +204,8 @@ class MiRCARTTool(): """XXX""" parentCanvas = None - # {{{ onMouseDown(self, event, mapPoint, isLeftDown, isRightDown): XXX - def onMouseDown(self, event, mapPoint, isLeftDown, isRightDown): - pass - # }}} - # {{{ onMouseMotion(self, event, mapPoint, isLeftDown, isRightDown): XXX - def onMouseMotion(self, event, mapPoint, isLeftDown, isRightDown): + # {{{ onMouseEvent(self, event, mapPoint, isDragging, isLeftDown, isRightDown): XXX + def onMouseEvent(self, event, mapPoint, isDragging, isLeftDown, isRightDown): pass # }}} # {{{ __init__(self, parentCanvas): initialisation method @@ -222,8 +216,8 @@ class MiRCARTTool(): class MiRCARTToolRect(MiRCARTTool): """XXX""" - # {{{ _draw(self, event, mapPoint, isLeftDown, isRightDown): XXX - def _draw(self, event, mapPoint, isLeftDown, isRightDown): + # {{{ onMouseEvent(self, event, mapPoint, isDragging, isLeftDown, isRightDown): XXX + def onMouseEvent(self, event, mapPoint, isDragging, isLeftDown, isRightDown): if isLeftDown: return [[False, [[0, 0, \ self.parentCanvas.mircFg, \ @@ -243,18 +237,6 @@ class MiRCARTToolRect(MiRCARTTool): self.parentCanvas.mircFg, \ self.parentCanvas.mircFg, " "]]]] # }}} - # {{{ onMouseDown(self, event, mapPoint, isLeftDown, isRightDown): XXX - def onMouseDown(self, event, mapPoint, isLeftDown, isRightDown): - return self._draw(event, mapPoint, isLeftDown, isRightDown) - # }}} - # {{{ onMouseMotion(self, event, mapPoint, isLeftDown, isRightDown): XXX - def onMouseMotion(self, event, mapPoint, isLeftDown, isRightDown): - return self._draw(event, mapPoint, isLeftDown, isRightDown) - # }}} - # {{{ __init__(self, parentCanvas): initialisation method - def __init__(self, parentCanvas): - super().__init__(parentCanvas) - # }}} class MiRCARTFrame(wx.Frame): """XXX""" From fcc4215b6826160dad2ec624c1a329d1de028542 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Sat, 6 Jan 2018 02:50:12 +0100 Subject: [PATCH 069/148] {IrcClient,IrcMiRCARTBot,MiRC{2png,ART}}.py: fix spaces & typo. --- IrcClient.py | 6 +++--- IrcMiRCARTBot.py | 6 +++--- MiRC2png.py | 6 +++--- MiRCART.py | 8 ++++---- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/IrcClient.py b/IrcClient.py index dc407be..5c59d7a 100644 --- a/IrcClient.py +++ b/IrcClient.py @@ -2,17 +2,17 @@ # # mirc2png -- convert ASCII w/ mIRC control codes to monospaced PNG (for EFnet #MiRCART) # Copyright (c) 2018 Lucio Andrés Illanes Albornoz -# +# # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: -# +# # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. -# +# # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE diff --git a/IrcMiRCARTBot.py b/IrcMiRCARTBot.py index f996f49..4b9b4ca 100755 --- a/IrcMiRCARTBot.py +++ b/IrcMiRCARTBot.py @@ -2,17 +2,17 @@ # # IrcMiRCARTBot.py -- IRC<->MiRC2png bot (for EFnet #MiRCART) # Copyright (c) 2018 Lucio Andrés Illanes Albornoz -# +# # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: -# +# # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. -# +# # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE diff --git a/MiRC2png.py b/MiRC2png.py index 7b1633e..ec5ca9d 100755 --- a/MiRC2png.py +++ b/MiRC2png.py @@ -2,17 +2,17 @@ # # MiRC2png.py -- convert ASCII w/ mIRC control codes to monospaced PNG (for EFnet #MiRCART) # Copyright (c) 2018 Lucio Andrés Illanes Albornoz -# +# # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: -# +# # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. -# +# # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE diff --git a/MiRCART.py b/MiRCART.py index 536113f..895ede0 100755 --- a/MiRCART.py +++ b/MiRCART.py @@ -2,17 +2,17 @@ # # MiRCART.py -- mIRC art editor for Windows & Linux # Copyright (c) 2018 Lucio Andrés Illanes Albornoz -# +# # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: -# +# # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. -# +# # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -97,7 +97,7 @@ class MiRCARTCanvas(wx.Panel): self._drawPatch(patch, eventDc, tmpDc, atPoint) self.parentFrame.onCanvasUpdate() # }}} - # {{{ _pushUndo(self, atPoint, patch): XXXX + # {{{ _pushUndo(self, atPoint, patch): XXX def _pushUndo(self, atPoint, patch, mapItem): if self.patchesUndoLevel > 0: del self.patchesUndo[0:self.patchesUndoLevel] From d93e1fecda5caa3dfc186d4bc3512ab66af627c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Sat, 6 Jan 2018 04:42:15 +0100 Subject: [PATCH 070/148] MiRCART.py:MiRCARTCanvasJournal: split from MiRCARTCanvas. --- MiRCART.py | 164 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 99 insertions(+), 65 deletions(-) diff --git a/MiRCART.py b/MiRCART.py index 895ede0..9095dd7 100755 --- a/MiRCART.py +++ b/MiRCART.py @@ -47,13 +47,92 @@ mircColours = [ ] # }}} +class MiRCARTCanvasJournal(): + """XXX""" + parentCanvas = None + patchesTmp = patchesUndo = patchesUndoLevel = None + + # {{{ _popTmp(self, eventDc, tmpDc): XXX + def _popTmp(self, eventDc, tmpDc): + if self.patchesTmp: + for patch in self.patchesTmp: + patch[2:] = self.parentCanvas._getMapCell([patch[0], patch[1]]) + self.parentCanvas.onJournalUpdate(True, \ + (patch[0:2]), patch, eventDc, tmpDc, (0, 0)) + self.patchesTmp = [] + # }}} + # {{{ _pushTmp(self, atPoint, patch): XXX + def _pushTmp(self, absMapPoint): + self.patchesTmp.append([*absMapPoint, None, None, None]) + # }}} + # {{{ _pushUndo(self, atPoint, patch): XXX + def _pushUndo(self, atPoint, patch, mapItem): + if self.patchesUndoLevel > 0: + del self.patchesUndo[0:self.patchesUndoLevel] + self.patchesUndoLevel = 0 + absMapPoint = self._relMapPointToAbsMapPoint((patch[0], patch[1]), atPoint) + self.patchesUndo.insert(0, ( \ + (absMapPoint[0], absMapPoint[1], mapItem[0], mapItem[1], mapItem[2]), \ + (absMapPoint[0], absMapPoint[1], patch[2], patch[3], patch[4]))) + # }}} + # {{{ _relMapPointToAbsMapPoint(self, relMapPoint, atPoint): XXX + def _relMapPointToAbsMapPoint(self, relMapPoint, atPoint): + return (atPoint[0] + relMapPoint[0], atPoint[1] + relMapPoint[1]) + # }}} + # {{{ merge(self, mapPatches, eventDc, tmpDc, atPoint): XXX + def merge(self, mapPatches, eventDc, tmpDc, atPoint): + for mapPatch in mapPatches: + mapPatchTmp = mapPatch[0] + if mapPatchTmp: + self._popTmp(eventDc, tmpDc) + for patch in mapPatch[1]: + absMapPoint = self._relMapPointToAbsMapPoint(patch[0:2], atPoint) + mapItem = self.parentCanvas._getMapCell(absMapPoint) + if mapPatchTmp: + self._pushTmp(absMapPoint) + self.parentCanvas.onJournalUpdate(mapPatchTmp, \ + absMapPoint, patch, eventDc, tmpDc, atPoint) + elif mapItem != patch[2:5]: + self._pushUndo(atPoint, patch, mapItem) + self.parentCanvas.onJournalUpdate(mapPatchTmp, \ + absMapPoint, patch, eventDc, tmpDc, atPoint) + # }}} + # {{{ redo(self): XXX + def redo(self): + if self.patchesUndoLevel > 0: + self.patchesUndoLevel -= 1 + redoPatch = self.patchesUndo[self.patchesUndoLevel][1] + self.parentCanvas.onJournalUpdate(False, \ + (redoPatch[0:2]), redoPatch, None, None, (0, 0)) + return True + else: + return False + # }}} + # {{{ undo(self): XXX + def undo(self): + if self.patchesUndo[self.patchesUndoLevel] != None: + undoPatch = self.patchesUndo[self.patchesUndoLevel][0] + self.patchesUndoLevel += 1 + self.parentCanvas.onJournalUpdate(False, \ + (undoPatch[0:2]), undoPatch, None, None, (0, 0)) + return True + else: + return False + # }}} + # {{{ __init__(self, parentCanvas): initialisation method + def __init__(self, parentCanvas): + self.parentCanvas = parentCanvas + self.patchesTmp = [] + self.patchesUndo = [None]; self.patchesUndoLevel = 0; + # }}} + class MiRCARTCanvas(wx.Panel): """XXX""" parentFrame = None canvasPos = canvasSize = canvasWinSize = cellPos = cellSize = None canvasBitmap = canvasMap = canvasTools = None mircBg = mircFg = mircBrushes = mircPens = None - patchesTmp = patchesUndo = patchesUndoLevel = None + canvasJournal = None # {{{ _drawPatch(self, patch, eventDc, tmpDc, atPoint): XXX def _drawPatch(self, patch, eventDc, tmpDc, atPoint): @@ -76,41 +155,6 @@ class MiRCARTCanvas(wx.Panel): def _getMapCell(self, absMapPoint): return self.canvasMap[absMapPoint[1]][absMapPoint[0]] # }}} - # {{{ _processMapPatches(self, mapPatches, eventDc, tmpDc, atPoint): XXX - def _processMapPatches(self, mapPatches, eventDc, tmpDc, atPoint): - for mapPatch in mapPatches: - mapPatchTmp = mapPatch[0] - if mapPatchTmp and self.patchesTmp: - for patch in self.patchesTmp: - patch[2:] = self._getMapCell([patch[0], patch[1]]) - self._drawPatch(patch, eventDc, tmpDc, (0, 0)) - self.patchesTmp = [] - for patch in mapPatch[1]: - absMapPoint = self._relMapPointToAbsMapPoint(patch[0:2], atPoint) - mapItem = self._getMapCell(absMapPoint) - if mapPatchTmp: - self.patchesTmp.append([*absMapPoint, None, None, None]) - self._drawPatch(patch, eventDc, tmpDc, atPoint) - elif mapItem != patch[2:5]: - self._pushUndo(atPoint, patch, mapItem) - self._setMapCell(absMapPoint, *patch[2:5]) - self._drawPatch(patch, eventDc, tmpDc, atPoint) - self.parentFrame.onCanvasUpdate() - # }}} - # {{{ _pushUndo(self, atPoint, patch): XXX - def _pushUndo(self, atPoint, patch, mapItem): - if self.patchesUndoLevel > 0: - del self.patchesUndo[0:self.patchesUndoLevel] - self.patchesUndoLevel = 0 - absMapPoint = self._relMapPointToAbsMapPoint((patch[0], patch[1]), atPoint) - self.patchesUndo.insert(0, ( \ - (absMapPoint[0], absMapPoint[1], mapItem[0], mapItem[1], mapItem[2]), \ - (absMapPoint[0], absMapPoint[1], patch[2], patch[3], patch[4]))) - # }}} - # {{{ _relMapPointToAbsMapPoint(self, relMapPoint, atPoint): XXX - def _relMapPointToAbsMapPoint(self, relMapPoint, atPoint): - return (atPoint[0] + relMapPoint[0], atPoint[1] + relMapPoint[1]) - # }}} # {{{ _relMapPointToAbsPoint(self, relMapPoint, atPoint): XXX def _relMapPointToAbsPoint(self, relMapPoint, atPoint): absX = (atPoint[0] + relMapPoint[0]) * self.cellSize[0] @@ -122,6 +166,19 @@ class MiRCARTCanvas(wx.Panel): self.canvasMap[absMapPoint[1]][absMapPoint[0]] = [colourFg, colourBg, char] # }}} + # {{{ onJournalUpdate(self, isTmp, absMapPoint, patch, eventDc, tmpDc, atPoint): + def onJournalUpdate(self, isTmp, absMapPoint, patch, eventDc, tmpDc, atPoint): + if eventDc == None: + eventDc = wx.ClientDC(self); tmpDc = wx.MemoryDC(); + if tmpDc == None: + tmpDc.SelectObject(self.canvasBitmap) + if isTmp == True: + self._drawPatch(patch, eventDc, tmpDc, atPoint) + else: + self._setMapCell(absMapPoint, *patch[2:5]) + self._drawPatch(patch, eventDc, tmpDc, atPoint) + self.parentFrame.onCanvasUpdate() + # }}} # {{{ onMouseEvent(self, event): XXX def onMouseEvent(self, event): eventObject = event.GetEventObject() @@ -132,7 +189,7 @@ class MiRCARTCanvas(wx.Panel): for tool in self.canvasTools: mapPatches = tool.onMouseEvent(event, mapPoint, event.Dragging(), \ event.LeftIsDown(), event.RightIsDown()) - self._processMapPatches(mapPatches, eventDc, tmpDc, mapPoint) + self.canvasJournal.merge(mapPatches, eventDc, tmpDc, mapPoint) # }}} # {{{ onPaint(self, event): XXX def onPaint(self, event): @@ -140,33 +197,11 @@ class MiRCARTCanvas(wx.Panel): # }}} # {{{ redo(self): XXX def redo(self): - if self.patchesUndoLevel > 0: - self.patchesUndoLevel -= 1 - redoPatch = self.patchesUndo[self.patchesUndoLevel][1] - self._setMapCell([redoPatch[0], redoPatch[1]], \ - redoPatch[2], redoPatch[3], redoPatch[4]) - eventDc = wx.ClientDC(self); tmpDc = wx.MemoryDC(); - tmpDc.SelectObject(self.canvasBitmap) - self._drawPatch(redoPatch, eventDc, tmpDc, (0, 0)) - self.parentFrame.onCanvasUpdate() - return True - else: - return False + return self.canvasJournal.redo() # }}} # {{{ undo(self): XXX def undo(self): - if self.patchesUndo[self.patchesUndoLevel] != None: - undoPatch = self.patchesUndo[self.patchesUndoLevel][0] - self._setMapCell([undoPatch[0], undoPatch[1]], \ - undoPatch[2], undoPatch[3], undoPatch[4]) - eventDc = wx.ClientDC(self); tmpDc = wx.MemoryDC(); - tmpDc.SelectObject(self.canvasBitmap) - self._drawPatch(undoPatch, eventDc, tmpDc, (0, 0)) - self.patchesUndoLevel += 1 - self.parentFrame.onCanvasUpdate() - return True - else: - return False + return self.canvasJournal.undo() # }}} # {{{ __init__(self, parent, parentFrame, canvasPos, cellSize, canvasSize, canvasTools): Initialisation method def __init__(self, parent, parentFrame, canvasPos, cellSize, canvasSize, canvasTools): @@ -191,8 +226,7 @@ class MiRCARTCanvas(wx.Panel): self.mircPens[mircColour] = wx.Pen( \ wx.Colour(mircColours[mircColour]), 1) - self.patchesTmp = [] - self.patchesUndo = [None]; self.patchesUndoLevel = 0; + self.canvasJournal = MiRCARTCanvasJournal(self) self.Bind(wx.EVT_LEFT_DOWN, self.onMouseEvent) self.Bind(wx.EVT_MOTION, self.onMouseEvent) @@ -409,11 +443,11 @@ class MiRCARTFrame(wx.Frame): # {{{ onCanvasUpdate(self): XXX def onCanvasUpdate(self): - if self.panelCanvas.patchesUndo[self.panelCanvas.patchesUndoLevel] != None: + if self.panelCanvas.canvasJournal.patchesUndo[self.panelCanvas.canvasJournal.patchesUndoLevel] != None: self.menuItemsById[self.CID_UNDO[0]].Enable(True) else: self.menuItemsById[self.CID_UNDO[0]].Enable(False) - if self.panelCanvas.patchesUndoLevel > 0: + if self.panelCanvas.canvasJournal.patchesUndoLevel > 0: self.menuItemsById[self.CID_REDO[0]].Enable(True) else: self.menuItemsById[self.CID_REDO[0]].Enable(False) From 0259662a4e27ff8385e9f40f36be1efefbd58377 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Sat, 6 Jan 2018 16:38:20 +0100 Subject: [PATCH 071/148] IrcClient.py:IrcClient.connect(): explicitly pass encoding="utf-8" to clientSocket.makeFile(). --- IrcClient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IrcClient.py b/IrcClient.py index 5c59d7a..0816558 100644 --- a/IrcClient.py +++ b/IrcClient.py @@ -52,7 +52,7 @@ class IrcClient: self.close(); return False; else: select.select([], [self.clientSocket.fileno()], []) - self.clientSocketFile = self.clientSocket.makefile() + self.clientSocketFile = self.clientSocket.makefile(encoding="utf-8") self.clientQueue = [] self.queue("NICK", self.clientNick) self.queue("USER", self.clientIdent, "0", "0", self.clientGecos) From 115991736a12a70cf4d4be14c06b6bd3272f1116 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Sun, 7 Jan 2018 02:08:35 +0100 Subject: [PATCH 072/148] MiRCART.py: hand off to MiRCARTFrame(). MiRCARTCanvas.py: split from MiRCART.py. MiRCARTCanvasJournal.py: split from MiRCART.py. MiRCARTColours.py: split from MiRCART.py. MiRCARTFrame.py: split from MiRCART.py. MiRCARTFromTextFile.py: initial implementation. MiRCARTToPastebin.py: initial implementation. MiRCARTToPngFile.py, IrcMiRCARTBot.py: renamed MiRC2png to MiRCARTToPngFile. MiRCARTToTextFile.py: split from MiRCART.py. MiRCARTTool.py: split from MiRCART.py. MiRCARTToolRect.py: split from MiRCART.py. README.md: updated. --- IrcMiRCARTBot.py | 5 +- MiRC2png.py | 256 -------------------- MiRCART.py | 516 +--------------------------------------- MiRCARTCanvas.py | 157 ++++++++++++ MiRCARTCanvasJournal.py | 105 ++++++++ MiRCARTColours.py | 47 ++++ MiRCARTFrame.py | 375 +++++++++++++++++++++++++++++ MiRCARTFromTextFile.py | 147 ++++++++++++ MiRCARTToPastebin.py | 55 +++++ MiRCARTToPngFile.py | 145 +++++++++++ MiRCARTToTextFile.py | 47 ++++ MiRCARTTool.py | 39 +++ MiRCARTToolRect.py | 52 ++++ README.md | 2 +- 14 files changed, 1177 insertions(+), 771 deletions(-) delete mode 100755 MiRC2png.py create mode 100644 MiRCARTCanvas.py create mode 100644 MiRCARTCanvasJournal.py create mode 100755 MiRCARTColours.py create mode 100644 MiRCARTFrame.py create mode 100644 MiRCARTFromTextFile.py create mode 100644 MiRCARTToPastebin.py create mode 100755 MiRCARTToPngFile.py create mode 100644 MiRCARTToTextFile.py create mode 100644 MiRCARTTool.py create mode 100644 MiRCARTToolRect.py diff --git a/IrcMiRCARTBot.py b/IrcMiRCARTBot.py index 4b9b4ca..6d75554 100755 --- a/IrcMiRCARTBot.py +++ b/IrcMiRCARTBot.py @@ -25,8 +25,9 @@ import base64 import os, sys, time import json -import IrcClient, MiRC2png +import IrcClient import requests, urllib.request +from MiRCARTToPngFile import MiRCARTToPngFile class IrcMiRCARTBot(IrcClient.IrcClient): """IRC<->MiRC2png bot""" @@ -138,7 +139,7 @@ class IrcMiRCARTBot(IrcClient.IrcClient): self._log("Unknown URL type specified!") self.queue("PRIVMSG", message[2], "4/!\\ Unknown URL type specified!") return - MiRC2png.MiRC2png(asciiTmpFilePath, imgTmpFilePath, "DejaVuSansMono.ttf", 11) + MiRCARTToPngFile(asciiTmpFilePath, "DejaVuSansMono.ttf", 11).export(imgTmpFilePath) imgurResponse = self._uploadToImgur(imgTmpFilePath, "MiRCART image", "MiRCART image", "c9a6efb3d7932fd") if imgurResponse[0] == 200: self._log("Uploaded as: {}".format(imgurResponse[1])) diff --git a/MiRC2png.py b/MiRC2png.py deleted file mode 100755 index ec5ca9d..0000000 --- a/MiRC2png.py +++ /dev/null @@ -1,256 +0,0 @@ -#!/usr/bin/env python3 -# -# MiRC2png.py -- convert ASCII w/ mIRC control codes to monospaced PNG (for EFnet #MiRCART) -# Copyright (c) 2018 Lucio Andrés Illanes Albornoz -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. -# - -from enum import Enum -from PIL import Image, ImageDraw, ImageFont -import string, sys - -class MiRC2png: - """Abstraction over ASCIIs containing mIRC control codes""" - inFilePath = inFile = None; - inLines = inColsMax = inRows = None; - - outFontFilePath = outFontSize = None; - outImg = outImgDraw = outImgFont = None; - outCurColourBg = outCurColourFg = None; - outCurX = outCurY = None; - - inCurBold = inCurItalic = inCurUnderline = None; - inCurColourSpec = None; - state = None; - inCurCol = inCurDigits = None; - - # {{{ _ColourMapBold: mIRC colour number to RGBA map given ^B (bold) - _ColourMapBold = [ - (255, 255, 255, 255), # White - (85, 85, 85, 255), # Grey - (85, 85, 255, 255), # Light Blue - (85, 255, 85, 255), # Light Green - (255, 85, 85, 255), # Light Red - (255, 85, 85, 255), # Light Red - (255, 85, 255, 255), # Pink - (255, 255, 85, 255), # Light Yellow - (255, 255, 85, 255), # Light Yellow - (85, 255, 85, 255), # Light Green - (85, 255, 255, 255), # Light Cyan - (85, 255, 255, 255), # Light Cyan - (85, 85, 255, 255), # Light Blue - (255, 85, 255, 255), # Pink - (85, 85, 85, 255), # Grey - (255, 255, 255, 255), # White - ] - # }}} - # {{{ _ColourMapNormal: mIRC colour number to RGBA map given none of ^[BFV_] (bold, italic, reverse, underline) - _ColourMapNormal = [ - (255, 255, 255, 255), # White - (0, 0, 0, 255), # Black - (0, 0, 187, 255), # Blue - (0, 187, 0, 255), # Green - (255, 85, 85, 255), # Light Red - (187, 0, 0, 255), # Red - (187, 0, 187, 255), # Purple - (187, 187, 0, 255), # Yellow - (255, 255, 85, 255), # Light Yellow - (85, 255, 85, 255), # Light Green - (0, 187, 187, 255), # Cyan - (85, 255, 255, 255), # Light Cyan - (85, 85, 255, 255), # Light Blue - (255, 85, 255, 255), # Pink - (85, 85, 85, 255), # Grey - (187, 187, 187, 255), # Light Grey - ] - # }}} - # {{{ _State(Enum): Parsing loop state - class _State(Enum): - STATE_CHAR = 1 - STATE_COLOUR_SPEC = 2 - STATE_CSPEC_DIGIT0 = 2 - STATE_CSPEC_DIGIT1 = 3 - # }}} - - # {{{ _countChar(self, char): XXX - def _countChar(self, char): - return True - # }}} - # {{{ _countColourSpecState(self, colourSpec): XXX - def _countColourSpecState(self, colourSpec): - return 0 - # }}} - # {{{ _render(self): XXX - def _render(self): - self.outCurX = 0; self.outCurY = 0; - for inCurRow in range(0, len(self.inLines)): - self.inCurBold = 0; self.inCurItalic = 0; self.inCurUnderline = 0; - self.inCurColourSpec = ""; self._State = self._State.STATE_CHAR; - self.inCurCol = 0; self.inCurDigits = 0; - while self.inCurCol < len(self.inLines[inCurRow]): - if self._State == self._State.STATE_CHAR: - self._parseAsChar( \ - self.inLines[inCurRow][self.inCurCol], \ - self._syncChar) - elif self._State == self._State.STATE_CSPEC_DIGIT0 \ - or self._State == self._State.STATE_CSPEC_DIGIT1: \ - self._parseAsColourSpec( \ - self.inLines[inCurRow][self.inCurCol], \ - self._syncColourSpecState) - self.outCurX = 0; self.outCurY += self.outImgFontSize[1]; - # }}} - # {{{ _getMaxCols(self, lines): Calculate widest row in lines, ignoring non-printable & mIRC control code sequences - def _getMaxCols(self, lines): - maxCols = 0; - for inCurRow in range(0, len(lines)): - self.inCurBold = 0; self.inCurItalic = 0; self.inCurUnderline = 0; - self.inCurColourSpec = ""; self._State = self._State.STATE_CHAR; - self.inCurCol = 0; self.inCurDigits = 0; curRowCols = 0; - while self.inCurCol < len(self.inLines[inCurRow]): - if self._State == self._State.STATE_CHAR: - if self._parseAsChar( \ - self.inLines[inCurRow][self.inCurCol], self._countChar): - curRowCols += 1 - elif self._State == self._State.STATE_CSPEC_DIGIT0 \ - or self._State == self._State.STATE_CSPEC_DIGIT1: - self._parseAsColourSpec( \ - self.inLines[inCurRow][self.inCurCol], \ - self._countColourSpecState) - maxCols = max(maxCols, curRowCols) - return maxCols - # }}} - # {{{ _parseAsChar(self, char, fn): Parse single character as regular character and mutate state - def _parseAsChar(self, char, fn): - if char == "": - self._State = self._State.STATE_CSPEC_DIGIT0; self.inCurCol += 1; - return False - else: - self.inCurCol += 1; return fn(char); - # }}} - # {{{ _parseAsColourSpec(self, char, fn): Parse single character as mIRC colour control code sequence and mutate state - def _parseAsColourSpec(self, char, fn): - if self._State == self._State.STATE_CSPEC_DIGIT0 \ - and char == ",": - self.inCurColourSpec += char; self.inCurCol += 1; - self._State = self._State.STATE_CSPEC_DIGIT1; - self.inCurDigits = 0 - return [False] - elif self._State == self._State.STATE_CSPEC_DIGIT0 \ - and char in set("0123456789") \ - and self.inCurDigits <= 1: - self.inCurColourSpec += char; self.inCurCol += 1; - self.inCurDigits += 1 - return [False] - elif self._State == self._State.STATE_CSPEC_DIGIT1 \ - and char in set("0123456789") \ - and self.inCurDigits <= 1: - self.inCurColourSpec += char; self.inCurCol += 1; - self.inCurDigits += 1 - return [False] - else: - result = fn(self.inCurColourSpec) - self.inCurColourSpec = ""; self._State = self._State.STATE_CHAR; - self.inCurDigits = 0 - return [True, result] - # }}} - # {{{ _syncChar(self, char): XXX - def _syncChar(self, char): - if char == "": - self.inCurBold = 0 if self.inCurBold else 1; - elif char == "": - self.inCurItalic = 0 if self.inCurItalic else 1; - elif char == "": - self.inCurBold = 0; self.inCurItalic = 0; self.inCurUnderline = 0; - self.inCurColourSpec = ""; - elif char == "": - self.outCurColourBg, self.outCurColourFg = self.outCurColourFg, self.outCurColourBg; - elif char == "": - self.inCurUnderline = 0 if self.inCurUnderline else 1; - elif char == " ": - if self.inCurBold: - colourBg = self._ColourMapBold[self.outCurColourBg] - else: - colourBg = self._ColourMapNormal[self.outCurColourBg] - self.outImgDraw.rectangle(((self.outCurX, self.outCurY), (self.outCurX + self.outImgFontSize[0], self.outCurY + self.outImgFontSize[1])), fill=colourBg) - if self.inCurUnderline: - self.outImgDraw.line((self.outCurX, self.outCurY + (self.outImgFontSize[1] - 2), self.outCurX + self.outImgFontSize[0], self.outCurY + (self.outImgFontSize[1] - 2)), fill=colourFg) - self.outCurX += self.outImgFontSize[0]; - else: - if self.inCurBold: - colourBg = self._ColourMapBold[self.outCurColourBg] - colourFg = self._ColourMapBold[self.outCurColourFg] - else: - colourBg = self._ColourMapNormal[self.outCurColourBg] - colourFg = self._ColourMapNormal[self.outCurColourFg] - self.outImgDraw.rectangle(((self.outCurX, self.outCurY), (self.outCurX + self.outImgFontSize[0], self.outCurY + self.outImgFontSize[1])), fill=colourBg) - # XXX implement italic - self.outImgDraw.text((self.outCurX, self.outCurY), char, colourFg, self.outImgFont) - if self.inCurUnderline: - self.outImgDraw.line((self.outCurX, self.outCurY + (self.outImgFontSize[1] - 2), self.outCurX + self.outImgFontSize[0], self.outCurY + (self.outImgFontSize[1] - 2)), fill=colourFg) - self.outCurX += self.outImgFontSize[0]; - return True - # }}} - # {{{ _syncColourSpecState(self, colourSpec): XXX - def _syncColourSpecState(self, colourSpec): - if len(colourSpec) > 0: - colourSpec = colourSpec.split(",") - if len(colourSpec) == 2: - self.outCurColourFg = int(colourSpec[0]) - self.outCurColourBg = int(colourSpec[1] or self.outCurColourBg) - elif len(colourSpec) == 1: - self.outCurColourFg = int(colourSpec[0]) - else: - self.outCurColourBg = 1; self.outCurColourFg = 15; - return True - # }}} - - # - # __init__(self, inFilePath, imgFilePath, fontFilePath="DejaVuSansMono.ttf", fontSize=11): initialisation method - def __init__(self, inFilePath, imgFilePath, fontFilePath="DejaVuSansMono.ttf", fontSize=11): - self.inFilePath = inFilePath; self.inFile = open(inFilePath, "r"); - self.inLines = self.inFile.readlines() - self.inColsMax = self._getMaxCols(self.inLines) - self.inRows = len(self.inLines) - self.outFontFilePath = fontFilePath; self.outFontSize = int(fontSize); - self.outImgFont = ImageFont.truetype(self.outFontFilePath, self.outFontSize) - self.outImgFontSize = list(self.outImgFont.getsize(" ")); self.outImgFontSize[1] += 3; - self.outImg = Image.new("RGBA", (self.inColsMax * self.outImgFontSize[0], self.inRows * self.outImgFontSize[1]), self._ColourMapNormal[1]) - self.outImgDraw = ImageDraw.Draw(self.outImg) - self.outCurColourBg = 1; self.outCurColourFg = 15; - self._render() - self.inFile.close(); - self.outImg.save(imgFilePath); - -# -# Entry point -def main(*argv): - MiRC2png(*argv[1:]) -if __name__ == "__main__": - if ((len(sys.argv) - 1) < 2)\ - or ((len(sys.argv) - 1) > 4): - print("usage: {} " \ - " " \ - " " \ - "[] " \ - "[]".format(sys.argv[0]), file=sys.stderr) - else: - main(*sys.argv) - -# vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/MiRCART.py b/MiRCART.py index 9095dd7..28e9d36 100755 --- a/MiRCART.py +++ b/MiRCART.py @@ -22,523 +22,15 @@ # SOFTWARE. # -import enum -import wx -import os, sys - -# {{{ mircColours: mIRC colour number to RGBA map given none of ^[BFV_] (bold, italic, reverse, underline) -mircColours = [ - (255, 255, 255, 255), # White - (0, 0, 0, 255), # Black - (0, 0, 187, 255), # Blue - (0, 187, 0, 255), # Green - (255, 85, 85, 255), # Light Red - (187, 0, 0, 255), # Red - (187, 0, 187, 255), # Purple - (187, 187, 0, 255), # Yellow - (255, 255, 85, 255), # Light Yellow - (85, 255, 85, 255), # Light Green - (0, 187, 187, 255), # Cyan - (85, 255, 255, 255), # Light Cyan - (85, 85, 255, 255), # Light Blue - (255, 85, 255, 255), # Pink - (85, 85, 85, 255), # Grey - (187, 187, 187, 255), # Light Grey -] -# }}} - -class MiRCARTCanvasJournal(): - """XXX""" - parentCanvas = None - patchesTmp = patchesUndo = patchesUndoLevel = None - - # {{{ _popTmp(self, eventDc, tmpDc): XXX - def _popTmp(self, eventDc, tmpDc): - if self.patchesTmp: - for patch in self.patchesTmp: - patch[2:] = self.parentCanvas._getMapCell([patch[0], patch[1]]) - self.parentCanvas.onJournalUpdate(True, \ - (patch[0:2]), patch, eventDc, tmpDc, (0, 0)) - self.patchesTmp = [] - # }}} - # {{{ _pushTmp(self, atPoint, patch): XXX - def _pushTmp(self, absMapPoint): - self.patchesTmp.append([*absMapPoint, None, None, None]) - # }}} - # {{{ _pushUndo(self, atPoint, patch): XXX - def _pushUndo(self, atPoint, patch, mapItem): - if self.patchesUndoLevel > 0: - del self.patchesUndo[0:self.patchesUndoLevel] - self.patchesUndoLevel = 0 - absMapPoint = self._relMapPointToAbsMapPoint((patch[0], patch[1]), atPoint) - self.patchesUndo.insert(0, ( \ - (absMapPoint[0], absMapPoint[1], mapItem[0], mapItem[1], mapItem[2]), \ - (absMapPoint[0], absMapPoint[1], patch[2], patch[3], patch[4]))) - # }}} - # {{{ _relMapPointToAbsMapPoint(self, relMapPoint, atPoint): XXX - def _relMapPointToAbsMapPoint(self, relMapPoint, atPoint): - return (atPoint[0] + relMapPoint[0], atPoint[1] + relMapPoint[1]) - # }}} - # {{{ merge(self, mapPatches, eventDc, tmpDc, atPoint): XXX - def merge(self, mapPatches, eventDc, tmpDc, atPoint): - for mapPatch in mapPatches: - mapPatchTmp = mapPatch[0] - if mapPatchTmp: - self._popTmp(eventDc, tmpDc) - for patch in mapPatch[1]: - absMapPoint = self._relMapPointToAbsMapPoint(patch[0:2], atPoint) - mapItem = self.parentCanvas._getMapCell(absMapPoint) - if mapPatchTmp: - self._pushTmp(absMapPoint) - self.parentCanvas.onJournalUpdate(mapPatchTmp, \ - absMapPoint, patch, eventDc, tmpDc, atPoint) - elif mapItem != patch[2:5]: - self._pushUndo(atPoint, patch, mapItem) - self.parentCanvas.onJournalUpdate(mapPatchTmp, \ - absMapPoint, patch, eventDc, tmpDc, atPoint) - # }}} - # {{{ redo(self): XXX - def redo(self): - if self.patchesUndoLevel > 0: - self.patchesUndoLevel -= 1 - redoPatch = self.patchesUndo[self.patchesUndoLevel][1] - self.parentCanvas.onJournalUpdate(False, \ - (redoPatch[0:2]), redoPatch, None, None, (0, 0)) - return True - else: - return False - # }}} - # {{{ undo(self): XXX - def undo(self): - if self.patchesUndo[self.patchesUndoLevel] != None: - undoPatch = self.patchesUndo[self.patchesUndoLevel][0] - self.patchesUndoLevel += 1 - self.parentCanvas.onJournalUpdate(False, \ - (undoPatch[0:2]), undoPatch, None, None, (0, 0)) - return True - else: - return False - # }}} - # {{{ __init__(self, parentCanvas): initialisation method - def __init__(self, parentCanvas): - self.parentCanvas = parentCanvas - self.patchesTmp = [] - self.patchesUndo = [None]; self.patchesUndoLevel = 0; - # }}} - -class MiRCARTCanvas(wx.Panel): - """XXX""" - parentFrame = None - canvasPos = canvasSize = canvasWinSize = cellPos = cellSize = None - canvasBitmap = canvasMap = canvasTools = None - mircBg = mircFg = mircBrushes = mircPens = None - canvasJournal = None - - # {{{ _drawPatch(self, patch, eventDc, tmpDc, atPoint): XXX - def _drawPatch(self, patch, eventDc, tmpDc, atPoint): - absPoint = self._relMapPointToAbsPoint((patch[0], patch[1]), atPoint) - brushFg = self.mircBrushes[patch[2]]; brushBg = self.mircBrushes[patch[3]]; - pen = self.mircPens[patch[2]] - for dc in (eventDc, tmpDc): - dc.SetBrush(brushFg); dc.SetBackground(brushBg); dc.SetPen(pen); - dc.DrawRectangle(absPoint[0], absPoint[1], self.cellSize[0], self.cellSize[1]) - # }}} - # {{{ _eventPointToMapPoint(self, eventPoint): XXX - def _eventPointToMapPoint(self, eventPoint): - rectX = eventPoint.x - (eventPoint.x % self.cellSize[0]) - rectY = eventPoint.y - (eventPoint.y % self.cellSize[1]) - mapX = int(rectX / self.cellSize[0] if rectX else 0) - mapY = int(rectY / self.cellSize[1] if rectY else 0) - return (mapX, mapY) - # }}} - # {{{ _getMapCell(self, absMapPoint): XXX - def _getMapCell(self, absMapPoint): - return self.canvasMap[absMapPoint[1]][absMapPoint[0]] - # }}} - # {{{ _relMapPointToAbsPoint(self, relMapPoint, atPoint): XXX - def _relMapPointToAbsPoint(self, relMapPoint, atPoint): - absX = (atPoint[0] + relMapPoint[0]) * self.cellSize[0] - absY = (atPoint[1] + relMapPoint[1]) * self.cellSize[1] - return (absX, absY) - # }}} - # {{{ _setMapCell(self, absMapPoint, colourFg, colourBg, char): XXX - def _setMapCell(self, absMapPoint, colourFg, colourBg, char): - self.canvasMap[absMapPoint[1]][absMapPoint[0]] = [colourFg, colourBg, char] - # }}} - - # {{{ onJournalUpdate(self, isTmp, absMapPoint, patch, eventDc, tmpDc, atPoint): - def onJournalUpdate(self, isTmp, absMapPoint, patch, eventDc, tmpDc, atPoint): - if eventDc == None: - eventDc = wx.ClientDC(self); tmpDc = wx.MemoryDC(); - if tmpDc == None: - tmpDc.SelectObject(self.canvasBitmap) - if isTmp == True: - self._drawPatch(patch, eventDc, tmpDc, atPoint) - else: - self._setMapCell(absMapPoint, *patch[2:5]) - self._drawPatch(patch, eventDc, tmpDc, atPoint) - self.parentFrame.onCanvasUpdate() - # }}} - # {{{ onMouseEvent(self, event): XXX - def onMouseEvent(self, event): - eventObject = event.GetEventObject() - eventDc = wx.ClientDC(self); tmpDc = wx.MemoryDC(); - tmpDc.SelectObject(self.canvasBitmap) - eventPoint = event.GetLogicalPosition(eventDc) - mapPoint = self._eventPointToMapPoint(eventPoint) - for tool in self.canvasTools: - mapPatches = tool.onMouseEvent(event, mapPoint, event.Dragging(), \ - event.LeftIsDown(), event.RightIsDown()) - self.canvasJournal.merge(mapPatches, eventDc, tmpDc, mapPoint) - # }}} - # {{{ onPaint(self, event): XXX - def onPaint(self, event): - eventDc = wx.BufferedPaintDC(self, self.canvasBitmap) - # }}} - # {{{ redo(self): XXX - def redo(self): - return self.canvasJournal.redo() - # }}} - # {{{ undo(self): XXX - def undo(self): - return self.canvasJournal.undo() - # }}} - # {{{ __init__(self, parent, parentFrame, canvasPos, cellSize, canvasSize, canvasTools): Initialisation method - def __init__(self, parent, parentFrame, canvasPos, cellSize, canvasSize, canvasTools): - self.parentFrame = parentFrame - canvasWinSize = (cellSize[0] * canvasSize[0], cellSize[1] * canvasSize[1]) - super().__init__(parent, pos=canvasPos, size=canvasWinSize) - self.canvasPos = canvasPos; self.canvasSize = canvasSize; self.canvasWinSize = canvasWinSize; - self.cellPos = (0, 0); self.cellSize = cellSize; - - self.canvasBitmap = wx.Bitmap(canvasWinSize) - self.canvasMap = [[[1, 1, " "] for x in range(canvasSize[0])] for y in range(canvasSize[1])] - self.canvasTools = [] - for canvasTool in canvasTools: - self.canvasTools.append(canvasTool(self)) - - self.mircBg = 1; self.mircFg = 4; - self.mircBrushes = [None for x in range(len(mircColours))] - self.mircPens = [None for x in range(len(mircColours))] - for mircColour in range(0, len(mircColours)): - self.mircBrushes[mircColour] = wx.Brush( \ - wx.Colour(mircColours[mircColour]), wx.BRUSHSTYLE_SOLID) - self.mircPens[mircColour] = wx.Pen( \ - wx.Colour(mircColours[mircColour]), 1) - - self.canvasJournal = MiRCARTCanvasJournal(self) - - self.Bind(wx.EVT_LEFT_DOWN, self.onMouseEvent) - self.Bind(wx.EVT_MOTION, self.onMouseEvent) - self.Bind(wx.EVT_PAINT, self.onPaint) - self.Bind(wx.EVT_RIGHT_DOWN, self.onMouseEvent) - # }}} - -class MiRCARTTool(): - """XXX""" - parentCanvas = None - - # {{{ onMouseEvent(self, event, mapPoint, isDragging, isLeftDown, isRightDown): XXX - def onMouseEvent(self, event, mapPoint, isDragging, isLeftDown, isRightDown): - pass - # }}} - # {{{ __init__(self, parentCanvas): initialisation method - def __init__(self, parentCanvas): - self.parentCanvas = parentCanvas - # }}} - -class MiRCARTToolRect(MiRCARTTool): - """XXX""" - - # {{{ onMouseEvent(self, event, mapPoint, isDragging, isLeftDown, isRightDown): XXX - def onMouseEvent(self, event, mapPoint, isDragging, isLeftDown, isRightDown): - if isLeftDown: - return [[False, [[0, 0, \ - self.parentCanvas.mircFg, \ - self.parentCanvas.mircFg, " "]]], - [True, [[0, 0, \ - self.parentCanvas.mircFg, \ - self.parentCanvas.mircFg, " "]]]] - elif isRightDown: - return [[False, [[0, 0, \ - self.parentCanvas.mircBg, \ - self.parentCanvas.mircBg, " "]]], \ - [True, [[0, 0, \ - self.parentCanvas.mircBg, \ - self.parentCanvas.mircBg, " "]]]] - else: - return [[True, [[0, 0, \ - self.parentCanvas.mircFg, \ - self.parentCanvas.mircFg, " "]]]] - # }}} - -class MiRCARTFrame(wx.Frame): - """XXX""" - panelSkin = panelCanvas = None - menuItemsById = menuBar = toolBar = accelTable = statusBar = None - - # {{{ Types - TID_COMMAND = (0x001) - TID_NOTHING = (0x002) - TID_MENU = (0x003) - TID_TOOLBAR = (0x004) - TID_ACCELS = (0x005) - # }}} - # {{{ Commands - # Id Type Id Labels Icon bitmap Accelerator - CID_NEW = (0x100, TID_COMMAND, "New", "&New", [wx.ART_NEW], None) - CID_OPEN = (0x101, TID_COMMAND, "Open", "&Open", [wx.ART_FILE_OPEN], None) - CID_SAVE = (0x102, TID_COMMAND, "Save", "&Save", [wx.ART_FILE_SAVE], None) - CID_SAVEAS = (0x103, TID_COMMAND, "Save As...", "Save &As...", [wx.ART_FILE_SAVE_AS], None) - CID_EXPORT_PASTEBIN = (0x104, TID_COMMAND, "Export to Pastebin...", "Export to Pasteb&in...", (), None) - CID_EXPORT_AS_PNG = (0x105, TID_COMMAND, "Export as PNG...", "Export as PN&G...", (), None) - CID_EXIT = (0x106, TID_COMMAND, "Exit", "E&xit", (), None) - CID_UNDO = (0x107, TID_COMMAND, "Undo", "&Undo", [wx.ART_UNDO], (wx.ACCEL_CTRL, ord("Z"))) - CID_REDO = (0x108, TID_COMMAND, "Redo", "&Redo", [wx.ART_REDO], (wx.ACCEL_CTRL, ord("Y"))) - CID_CUT = (0x109, TID_COMMAND, "Cut", "Cu&t", [wx.ART_CUT], None) - CID_COPY = (0x10a, TID_COMMAND, "Copy", "&Copy", [wx.ART_COPY], None) - CID_PASTE = (0x10b, TID_COMMAND, "Paste", "&Paste", [wx.ART_PASTE], None) - CID_DELETE = (0x10c, TID_COMMAND, "Delete", "De&lete", [wx.ART_DELETE], None) - CID_INCRBRUSH = (0x10d, TID_COMMAND, "Increase brush size", "&Increase brush size", [wx.ART_PLUS], None) - CID_DECRBRUSH = (0x10e, TID_COMMAND, "Decrease brush size", "&Decrease brush size", [wx.ART_MINUS], None) - CID_SOLIDBRUSH = (0x10f, TID_COMMAND, "Solid brush", "&Solid brush", [None], None) - CID_RECT = (0x110, TID_COMMAND, "Rectangle", "&Rectangle", [None], None) - CID_CIRCLE = (0x111, TID_COMMAND, "Circle", "&Circle", [None], None) - CID_LINE = (0x112, TID_COMMAND, "Line", "&Line", [None], None) - CID_COLOUR00 = (0x113, TID_COMMAND, "Colour #00", "Colour #00", mircColours[0], None) - CID_COLOUR01 = (0x114, TID_COMMAND, "Colour #01", "Colour #01", mircColours[1], None) - CID_COLOUR02 = (0x115, TID_COMMAND, "Colour #02", "Colour #02", mircColours[2], None) - CID_COLOUR03 = (0x116, TID_COMMAND, "Colour #03", "Colour #03", mircColours[3], None) - CID_COLOUR04 = (0x117, TID_COMMAND, "Colour #04", "Colour #04", mircColours[4], None) - CID_COLOUR05 = (0x118, TID_COMMAND, "Colour #05", "Colour #05", mircColours[5], None) - CID_COLOUR06 = (0x119, TID_COMMAND, "Colour #06", "Colour #06", mircColours[6], None) - CID_COLOUR07 = (0x11a, TID_COMMAND, "Colour #07", "Colour #07", mircColours[7], None) - CID_COLOUR08 = (0x11b, TID_COMMAND, "Colour #08", "Colour #08", mircColours[8], None) - CID_COLOUR09 = (0x11c, TID_COMMAND, "Colour #09", "Colour #09", mircColours[9], None) - CID_COLOUR10 = (0x11d, TID_COMMAND, "Colour #10", "Colour #10", mircColours[10], None) - CID_COLOUR11 = (0x11e, TID_COMMAND, "Colour #11", "Colour #11", mircColours[11], None) - CID_COLOUR12 = (0x11f, TID_COMMAND, "Colour #12", "Colour #12", mircColours[12], None) - CID_COLOUR13 = (0x120, TID_COMMAND, "Colour #13", "Colour #13", mircColours[13], None) - CID_COLOUR14 = (0x121, TID_COMMAND, "Colour #14", "Colour #14", mircColours[14], None) - CID_COLOUR15 = (0x122, TID_COMMAND, "Colour #15", "Colour #15", mircColours[15], None) - # }}} - # {{{ Non-items - NID_MENU_SEP = (0x200, TID_NOTHING) - NID_TOOLBAR_SEP = (0x201, TID_NOTHING) - # }}} - # {{{ Menus - MID_FILE = (0x300, TID_MENU, "File", "&File", ( \ - CID_NEW, CID_OPEN, CID_SAVE, CID_SAVEAS, NID_MENU_SEP, \ - CID_EXPORT_PASTEBIN, CID_EXPORT_AS_PNG, NID_MENU_SEP, \ - CID_EXIT)) - MID_EDIT = (0x301, TID_MENU, "Edit", "&Edit", ( \ - CID_UNDO, CID_REDO, NID_MENU_SEP, \ - CID_CUT, CID_COPY, CID_PASTE, CID_DELETE, NID_MENU_SEP, \ - CID_INCRBRUSH, CID_DECRBRUSH, CID_SOLIDBRUSH)) - MID_TOOLS = (0x302, TID_MENU, "Tools", "&Tools", ( \ - CID_RECT, CID_CIRCLE, CID_LINE)) - # }}} - # {{{ Toolbars - BID_TOOLBAR = (0x400, TID_TOOLBAR, ( \ - CID_NEW, CID_OPEN, CID_SAVE, CID_SAVEAS, NID_TOOLBAR_SEP, \ - CID_UNDO, CID_REDO, NID_TOOLBAR_SEP, \ - CID_CUT, CID_COPY, CID_PASTE, CID_DELETE, NID_TOOLBAR_SEP, \ - CID_INCRBRUSH, CID_DECRBRUSH, CID_SOLIDBRUSH, NID_TOOLBAR_SEP, \ - CID_RECT, CID_CIRCLE, CID_LINE, NID_TOOLBAR_SEP, \ - CID_COLOUR00, CID_COLOUR01, CID_COLOUR02, CID_COLOUR03, CID_COLOUR04, \ - CID_COLOUR05, CID_COLOUR06, CID_COLOUR07, CID_COLOUR08, CID_COLOUR09, \ - CID_COLOUR10, CID_COLOUR11, CID_COLOUR12, CID_COLOUR13, CID_COLOUR14, \ - CID_COLOUR15)) - # }}} - # {{{ Accelerators (hotkeys) - AID_EDIT = (0x500, TID_ACCELS, (CID_UNDO, CID_REDO)) - # }}} - - # {{{ _drawIcon(self, solidColour): XXX - def _drawIcon(self, solidColour): - iconBitmap = wx.Bitmap((16,16)) - iconDc = wx.MemoryDC(); iconDc.SelectObject(iconBitmap); - iconBrush = wx.Brush(wx.Colour(solidColour), wx.BRUSHSTYLE_SOLID) - iconDc.SetBrush(iconBrush); iconDc.SetBackground(iconBrush); - iconDc.SetPen(wx.Pen(wx.Colour(solidColour), 1)) - iconDc.DrawRectangle(0, 0, 16, 16) - return iconBitmap - # }}} - # {{{ _initAccelTable(self, accelsDescr, handler): XXX - def _initAccelTable(self, accelsDescr, handler): - accelTableEntries = [wx.AcceleratorEntry() for n in range(0, len(accelsDescr[2]))] - for numAccel in range(0, len(accelsDescr[2])): - accelDescr = accelsDescr[2][numAccel] - if accelDescr[5] != None: - accelTableEntries[numAccel].Set(accelDescr[5][0], accelDescr[5][1], accelDescr[0]) - self.Bind(wx.EVT_MENU, handler, id=accelDescr[0]) - return accelTableEntries - # }}} - # {{{ _initMenus(self, menuBar, menusDescr, handler): XXX - def _initMenus(self, menuBar, menusDescr, handler): - for menuDescr in menusDescr: - menuWindow = wx.Menu() - for menuItem in menuDescr[4]: - if menuItem == self.NID_MENU_SEP: - menuWindow.AppendSeparator() - else: - menuItemWindow = menuWindow.Append(menuItem[0], menuItem[3], menuItem[2]) - self.menuItemsById[menuItem[0]] = menuItemWindow - self.Bind(wx.EVT_MENU, handler, menuItemWindow) - menuBar.Append(menuWindow, menuDescr[3]) - # }}} - # {{{ _initToolBars(self, toolBar, toolBarsDescr, handler): XXX - def _initToolBars(self, toolBar, toolBarsDescr, handler): - for toolBarDescr in toolBarsDescr: - for toolBarItem in toolBarDescr[2]: - if toolBarItem == self.NID_TOOLBAR_SEP: - toolBar.AddSeparator() - else: - if len(toolBarItem[4]) == 4: - toolBarItemIcon = self._drawIcon(toolBarItem[4]) - elif len(toolBarItem[4]) == 1 \ - and toolBarItem[4][0] != None: - toolBarItemIcon = wx.ArtProvider.GetBitmap( \ - toolBarItem[4][0], wx.ART_TOOLBAR, (16,16)) - else: - toolBarItemIcon = wx.ArtProvider.GetBitmap( \ - wx.ART_HELP, wx.ART_TOOLBAR, (16,16)) - toolBarItemWindow = self.toolBar.AddTool( \ - toolBarItem[0], toolBarItem[2], toolBarItemIcon) - self.Bind(wx.EVT_TOOL, handler, toolBarItemWindow) - self.Bind(wx.EVT_TOOL_RCLICKED, handler, toolBarItemWindow) - # }}} - # {{{ _saveAs(self, pathName): XXX - def _saveAs(self, pathName): - try: - with open(pathName, "w") as file: - canvasMap = self.panelCanvas.canvasMap - canvasHeight = self.panelCanvas.canvasSize[1] - canvasWidth = self.panelCanvas.canvasSize[0] - for canvasRow in range(0, canvasHeight): - colourLastBg = colourLastFg = None; - for canvasCol in range(0, canvasWidth): - canvasColBg = canvasMap[canvasRow][canvasCol][0] - canvasColFg = canvasMap[canvasRow][canvasCol][1] - canvasColText = canvasMap[canvasRow][canvasCol][2] - if colourLastBg != canvasColBg \ - or colourLastFg != canvasColFg: - colourLastBg = canvasColBg; colourLastFg = canvasColFg; - file.write("" + str(canvasColFg) + "," + str(canvasColBg)) - file.write(canvasColText) - file.write("\n") - return [True] - except IOError as error: - return [False, error] - # }}} - # {{{ _updateStatusBar(self): XXX - def _updateStatusBar(self): - text = "Foreground colour:" - text += " " + str(self.panelCanvas.mircFg) - text += " | " - text += "Background colour:" - text += " " + str(self.panelCanvas.mircBg) - self.statusBar.SetStatusText(text) - # }}} - - # {{{ onCanvasUpdate(self): XXX - def onCanvasUpdate(self): - if self.panelCanvas.canvasJournal.patchesUndo[self.panelCanvas.canvasJournal.patchesUndoLevel] != None: - self.menuItemsById[self.CID_UNDO[0]].Enable(True) - else: - self.menuItemsById[self.CID_UNDO[0]].Enable(False) - if self.panelCanvas.canvasJournal.patchesUndoLevel > 0: - self.menuItemsById[self.CID_REDO[0]].Enable(True) - else: - self.menuItemsById[self.CID_REDO[0]].Enable(False) - # }}} - # {{{ onFrameCommand(self, event): XXX - def onFrameCommand(self, event): - cid = event.GetId() - if cid == self.CID_NEW[0]: - pass - elif cid == self.CID_OPEN[0]: - pass - elif cid == self.CID_SAVE[0]: - pass - elif cid == self.CID_SAVEAS[0]: - with wx.FileDialog(self, self.CID_SAVEAS[2], os.getcwd(), "", \ - "*.txt", wx.FD_SAVE|wx.FD_OVERWRITE_PROMPT) as dialog: - if dialog.ShowModal() == wx.ID_CANCEL: - return - else: - self._saveAs(dialog.GetPath()) - elif cid == self.CID_EXPORT_PASTEBIN[0]: - pass - elif cid == self.CID_EXPORT_AS_PNG[0]: - pass - elif cid == self.CID_EXIT[0]: - self.Close(True) - elif cid == self.CID_UNDO[0]: - self.panelCanvas.undo() - elif cid == self.CID_REDO[0]: - self.panelCanvas.redo() - elif cid == self.CID_CUT[0]: - pass - elif cid == self.CID_COPY[0]: - pass - elif cid == self.CID_PASTE[0]: - pass - elif cid == self.CID_DELETE[0]: - pass - elif cid == self.CID_INCRBRUSH[0]: - pass - elif cid == self.CID_DECRBRUSH[0]: - pass - elif cid == self.CID_SOLIDBRUSH[0]: - pass - elif cid == self.CID_RECT[0]: - pass - elif cid == self.CID_CIRCLE[0]: - pass - elif cid == self.CID_LINE[0]: - pass - elif cid >= self.CID_COLOUR00[0] \ - and cid <= self.CID_COLOUR15[0]: - numColour = cid - self.CID_COLOUR00[0] - if event.GetEventType() == wx.wxEVT_TOOL: - self.panelCanvas.mircFg = numColour - elif event.GetEventType() == wx.wxEVT_TOOL_RCLICKED: - self.panelCanvas.mircBg = numColour - self._updateStatusBar() - # }}} - # {{{ __init__(self, parent, appSize=(800, 600), canvasPos=(25, 50), cellSize=(7, 14), canvasSize=(80, 25)): initialisation method - def __init__(self, parent, appSize=(800, 600), canvasPos=(25, 50), cellSize=(7, 14), canvasSize=(80, 25)): - super().__init__(parent, wx.ID_ANY, "MiRCART", size=appSize) - self.panelSkin = wx.Panel(self, wx.ID_ANY) - self.panelCanvas = MiRCARTCanvas(self.panelSkin, \ - parentFrame=self, canvasPos=canvasPos, cellSize=cellSize, \ - canvasSize=canvasSize, canvasTools=[MiRCARTToolRect]) - - self.menuItemsById = {}; self.menuBar = wx.MenuBar(); - self._initMenus(self.menuBar, \ - [self.MID_FILE, self.MID_EDIT, self.MID_TOOLS], self.onFrameCommand) - self.SetMenuBar(self.menuBar) - - self.toolBar = wx.ToolBar(self.panelSkin, -1, \ - style=wx.HORIZONTAL|wx.TB_FLAT|wx.TB_NODIVIDER) - self.toolBar.SetToolBitmapSize((16,16)) - self._initToolBars(self.toolBar, [self.BID_TOOLBAR], self.onFrameCommand) - self.toolBar.Realize(); self.toolBar.Fit(); - - self.accelTable = wx.AcceleratorTable( \ - self._initAccelTable(self.AID_EDIT, self.onFrameCommand)) - self.SetAcceleratorTable(self.accelTable) - - self.statusBar = self.CreateStatusBar(); self._updateStatusBar(); - self.SetFocus(); self.Show(True); self.onCanvasUpdate(); - # }}} +from MiRCARTFrame import MiRCARTFrame +from MiRCARTToolRect import MiRCARTToolRect +import sys, wx # # Entry point def main(*argv): wxApp = wx.App(False) - MiRCARTFrame(None) + MiRCARTFrame(None, canvasTools=[MiRCARTToolRect]) wxApp.MainLoop() if __name__ == "__main__": main(*sys.argv) diff --git a/MiRCARTCanvas.py b/MiRCARTCanvas.py new file mode 100644 index 0000000..3743b5c --- /dev/null +++ b/MiRCARTCanvas.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python3 +# +# MiRCARTCanvas.py -- XXX +# Copyright (c) 2018 Lucio Andrés Illanes Albornoz +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +from MiRCARTCanvasJournal import MiRCARTCanvasJournal +from MiRCARTColours import MiRCARTColours +import wx + +class MiRCARTCanvas(wx.Panel): + """XXX""" + parentFrame = None + canvasPos = canvasSize = canvasWinSize = cellPos = cellSize = None + canvasBitmap = canvasMap = canvasTools = None + mircBg = mircFg = mircBrushes = mircPens = None + canvasJournal = None + + # {{{ _drawPatch(self, patch, eventDc, tmpDc, atPoint): XXX + def _drawPatch(self, patch, eventDc, tmpDc, atPoint): + absPoint = self._relMapPointToAbsPoint((patch[0], patch[1]), atPoint) + if patch[4] == " ": + brushFg = self.mircBrushes[patch[3]]; brushBg = self.mircBrushes[patch[3]]; + pen = self.mircPens[patch[3]] + else: + brushFg = self.mircBrushes[patch[2]]; brushBg = self.mircBrushes[patch[3]]; + pen = self.mircPens[patch[2]] + for dc in (eventDc, tmpDc): + dc.SetBrush(brushFg); dc.SetBackground(brushBg); dc.SetPen(pen); + dc.DrawRectangle(absPoint[0], absPoint[1], self.cellSize[0], self.cellSize[1]) + # }}} + # {{{ _eventPointToMapPoint(self, eventPoint): XXX + def _eventPointToMapPoint(self, eventPoint): + rectX = eventPoint.x - (eventPoint.x % self.cellSize[0]) + rectY = eventPoint.y - (eventPoint.y % self.cellSize[1]) + mapX = int(rectX / self.cellSize[0] if rectX else 0) + mapY = int(rectY / self.cellSize[1] if rectY else 0) + return (mapX, mapY) + # }}} + # {{{ _getMapCell(self, absMapPoint): XXX + def _getMapCell(self, absMapPoint): + return self.canvasMap[absMapPoint[1]][absMapPoint[0]] + # }}} + # {{{ _relMapPointToAbsPoint(self, relMapPoint, atPoint): XXX + def _relMapPointToAbsPoint(self, relMapPoint, atPoint): + absX = (atPoint[0] + relMapPoint[0]) * self.cellSize[0] + absY = (atPoint[1] + relMapPoint[1]) * self.cellSize[1] + return (absX, absY) + # }}} + # {{{ _setMapCell(self, absMapPoint, colourFg, colourBg, char): XXX + def _setMapCell(self, absMapPoint, colourFg, colourBg, char): + self.canvasMap[absMapPoint[1]][absMapPoint[0]] = [colourFg, colourBg, char] + # }}} + # {{{ onClose(self, event): XXX + def onClose(self, event): + self.Destroy(); self.__del__(); + # }}} + # {{{ onJournalUpdate(self, isTmp, absMapPoint, patch, eventDc, tmpDc, atPoint): + def onJournalUpdate(self, isTmp, absMapPoint, patch, eventDc, tmpDc, atPoint): + if eventDc == None: + eventDc = wx.ClientDC(self); tmpDc = wx.MemoryDC(); + if tmpDc == None: + tmpDc.SelectObject(self.canvasBitmap) + if isTmp == True: + self._drawPatch(patch, eventDc, tmpDc, atPoint) + else: + self._setMapCell(absMapPoint, *patch[2:5]) + self._drawPatch(patch, eventDc, tmpDc, atPoint) + self.parentFrame.onCanvasUpdate() + # }}} + # {{{ onMouseEvent(self, event): XXX + def onMouseEvent(self, event): + eventObject = event.GetEventObject() + eventDc = wx.ClientDC(self); tmpDc = wx.MemoryDC(); + tmpDc.SelectObject(self.canvasBitmap) + eventPoint = event.GetLogicalPosition(eventDc) + mapPoint = self._eventPointToMapPoint(eventPoint) + for tool in self.canvasTools: + mapPatches = tool.onMouseEvent(event, mapPoint, event.Dragging(), \ + event.LeftIsDown(), event.RightIsDown()) + self.canvasJournal.merge(mapPatches, eventDc, tmpDc, mapPoint) + # }}} + # {{{ onPaint(self, event): XXX + def onPaint(self, event): + eventDc = wx.BufferedPaintDC(self, self.canvasBitmap) + # }}} + # {{{ redo(self): XXX + def redo(self): + return self.canvasJournal.redo() + # }}} + # {{{ undo(self): XXX + def undo(self): + return self.canvasJournal.undo() + # }}} + # {{{ __del__(self): destructor method + def __del__(self): + if self.canvasBitmap != None: + self.canvasBitmap.Destroy(); self.canvasBitmap = None; + for brush in self.mircBrushes or []: + brush.Destroy() + self.mircBrushes = None + for pen in self.mircPens or []: + pen.Destroy() + self.mircPens = None + # }}} + + # + # _init__(self, parent, parentFrame, canvasPos, cellSize, canvasSize, canvasTools): initialisation method + def __init__(self, parent, parentFrame, canvasPos, cellSize, canvasSize, canvasTools): + self.parentFrame = parentFrame + canvasWinSize = (cellSize[0] * canvasSize[0], cellSize[1] * canvasSize[1]) + super().__init__(parent, pos=canvasPos, size=canvasWinSize) + self.canvasPos = canvasPos; self.canvasSize = canvasSize; self.canvasWinSize = canvasWinSize; + self.cellPos = (0, 0); self.cellSize = cellSize; + + self.canvasBitmap = wx.Bitmap(canvasWinSize) + self.canvasMap = [[[1, 1, " "] for x in range(canvasSize[0])] for y in range(canvasSize[1])] + self.canvasTools = [] + for canvasTool in canvasTools: + self.canvasTools.append(canvasTool(self)) + + self.mircBg = 1; self.mircFg = 4; + self.mircBrushes = [None for x in range(len(MiRCARTColours))] + self.mircPens = [None for x in range(len(MiRCARTColours))] + for mircColour in range(0, len(MiRCARTColours)): + self.mircBrushes[mircColour] = wx.Brush( \ + wx.Colour(MiRCARTColours[mircColour]), wx.BRUSHSTYLE_SOLID) + self.mircPens[mircColour] = wx.Pen( \ + wx.Colour(MiRCARTColours[mircColour]), 1) + + self.canvasJournal = MiRCARTCanvasJournal(self) + + self.Bind(wx.EVT_CLOSE, self.onClose) + self.Bind(wx.EVT_LEFT_DOWN, self.onMouseEvent) + self.Bind(wx.EVT_MOTION, self.onMouseEvent) + self.Bind(wx.EVT_PAINT, self.onPaint) + self.Bind(wx.EVT_RIGHT_DOWN, self.onMouseEvent) + +# vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/MiRCARTCanvasJournal.py b/MiRCARTCanvasJournal.py new file mode 100644 index 0000000..03e5f93 --- /dev/null +++ b/MiRCARTCanvasJournal.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +# +# MiRCARTCanvasJournal.py -- XXX +# Copyright (c) 2018 Lucio Andrés Illanes Albornoz +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +class MiRCARTCanvasJournal(): + """XXX""" + parentCanvas = None + patchesTmp = patchesUndo = patchesUndoLevel = None + + # {{{ _popTmp(self, eventDc, tmpDc): XXX + def _popTmp(self, eventDc, tmpDc): + if self.patchesTmp: + for patch in self.patchesTmp: + patch[2:] = self.parentCanvas._getMapCell([patch[0], patch[1]]) + self.parentCanvas.onJournalUpdate(True, \ + (patch[0:2]), patch, eventDc, tmpDc, (0, 0)) + self.patchesTmp = [] + # }}} + # {{{ _pushTmp(self, atPoint, patch): XXX + def _pushTmp(self, absMapPoint): + self.patchesTmp.append([*absMapPoint, None, None, None]) + # }}} + # {{{ _pushUndo(self, atPoint, patch): XXX + def _pushUndo(self, atPoint, patch, mapItem): + if self.patchesUndoLevel > 0: + del self.patchesUndo[0:self.patchesUndoLevel] + self.patchesUndoLevel = 0 + absMapPoint = self._relMapPointToAbsMapPoint((patch[0], patch[1]), atPoint) + self.patchesUndo.insert(0, ( \ + (absMapPoint[0], absMapPoint[1], mapItem[0], mapItem[1], mapItem[2]), \ + (absMapPoint[0], absMapPoint[1], patch[2], patch[3], patch[4]))) + # }}} + # {{{ _relMapPointToAbsMapPoint(self, relMapPoint, atPoint): XXX + def _relMapPointToAbsMapPoint(self, relMapPoint, atPoint): + return (atPoint[0] + relMapPoint[0], atPoint[1] + relMapPoint[1]) + # }}} + # {{{ merge(self, mapPatches, eventDc, tmpDc, atPoint): XXX + def merge(self, mapPatches, eventDc, tmpDc, atPoint): + for mapPatch in mapPatches: + mapPatchTmp = mapPatch[0] + if mapPatchTmp: + self._popTmp(eventDc, tmpDc) + for patch in mapPatch[1]: + absMapPoint = self._relMapPointToAbsMapPoint(patch[0:2], atPoint) + mapItem = self.parentCanvas._getMapCell(absMapPoint) + if mapPatchTmp: + self._pushTmp(absMapPoint) + self.parentCanvas.onJournalUpdate(mapPatchTmp, \ + absMapPoint, patch, eventDc, tmpDc, atPoint) + elif mapItem != patch[2:5]: + self._pushUndo(atPoint, patch, mapItem) + self.parentCanvas.onJournalUpdate(mapPatchTmp, \ + absMapPoint, patch, eventDc, tmpDc, atPoint) + # }}} + # {{{ redo(self): XXX + def redo(self): + if self.patchesUndoLevel > 0: + self.patchesUndoLevel -= 1 + redoPatch = self.patchesUndo[self.patchesUndoLevel][1] + self.parentCanvas.onJournalUpdate(False, \ + (redoPatch[0:2]), redoPatch, None, None, (0, 0)) + return True + else: + return False + # }}} + # {{{ undo(self): XXX + def undo(self): + if self.patchesUndo[self.patchesUndoLevel] != None: + undoPatch = self.patchesUndo[self.patchesUndoLevel][0] + self.patchesUndoLevel += 1 + self.parentCanvas.onJournalUpdate(False, \ + (undoPatch[0:2]), undoPatch, None, None, (0, 0)) + return True + else: + return False + # }}} + + # + # __init__(self, parentCanvas): initialisation method + def __init__(self, parentCanvas): + self.parentCanvas = parentCanvas + self.patchesTmp = [] + self.patchesUndo = [None]; self.patchesUndoLevel = 0; + +# vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/MiRCARTColours.py b/MiRCARTColours.py new file mode 100755 index 0000000..9489b08 --- /dev/null +++ b/MiRCARTColours.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +# +# MiRCARTColours.py -- XXX +# Copyright (c) 2018 Lucio Andrés Illanes Albornoz +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +# +# MiRCARTColours: mIRC colour number to RGBA map given none of ^[BFV_] (bold, italic, reverse, underline) +# +MiRCARTColours = [ + (255, 255, 255, 255), # White + (0, 0, 0, 255), # Black + (0, 0, 187, 255), # Blue + (0, 187, 0, 255), # Green + (255, 85, 85, 255), # Light Red + (187, 0, 0, 255), # Red + (187, 0, 187, 255), # Purple + (187, 187, 0, 255), # Yellow + (255, 255, 85, 255), # Light Yellow + (85, 255, 85, 255), # Light Green + (0, 187, 187, 255), # Cyan + (85, 255, 255, 255), # Light Cyan + (85, 85, 255, 255), # Light Blue + (255, 85, 255, 255), # Pink + (85, 85, 85, 255), # Grey + (187, 187, 187, 255), # Light Grey +] + +# vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/MiRCARTFrame.py b/MiRCARTFrame.py new file mode 100644 index 0000000..8c8ae20 --- /dev/null +++ b/MiRCARTFrame.py @@ -0,0 +1,375 @@ +#!/usr/bin/env python3 +# +# MiRCARTFrame.py -- XXX +# Copyright (c) 2018 Lucio Andrés Illanes Albornoz +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +from MiRCARTCanvas import MiRCARTCanvas +from MiRCARTColours import MiRCARTColours +from MiRCARTFromTextFile import MiRCARTFromTextFile +from MiRCARTToTextFile import MiRCARTToTextFile +import os, wx + +try: + from MiRCARTToPastebin import MiRCARTToPastebin + haveMiRCARTToPastebin = True +except ImportError: + haveMiRCARTToPastebin = False + +try: + from MiRCARTToPngFile import MiRCARTToPngFile + haveMiRCARTToPngFile = True +except ImportError: + haveMiRCARTToPngFile = False + +class MiRCARTFrame(wx.Frame): + """XXX""" + panelSkin = panelCanvas = canvasPathName = None + canvasPos = canvasSize = canvasTools = cellSize = None + menuItemsById = menuBar = toolBar = accelTable = statusBar = None + + # {{{ Types + TID_COMMAND = (0x001) + TID_NOTHING = (0x002) + TID_MENU = (0x003) + TID_TOOLBAR = (0x004) + TID_ACCELS = (0x005) + # }}} + # {{{ Commands + # Id Type Id Labels Icon bitmap Accelerator + CID_NEW = (0x100, TID_COMMAND, "New", "&New", [wx.ART_NEW], None) + CID_OPEN = (0x101, TID_COMMAND, "Open", "&Open", [wx.ART_FILE_OPEN], None) + CID_SAVE = (0x102, TID_COMMAND, "Save", "&Save", [wx.ART_FILE_SAVE], None) + CID_SAVEAS = (0x103, TID_COMMAND, "Save As...", "Save &As...", [wx.ART_FILE_SAVE_AS], None) + CID_EXPORT_AS_PNG = (0x104, TID_COMMAND, "Export as PNG...", "Export as PN&G...", (), None) + CID_EXPORT_PASTEBIN = (0x105, TID_COMMAND, "Export to Pastebin...", "Export to Pasteb&in...", (), None) + CID_EXIT = (0x106, TID_COMMAND, "Exit", "E&xit", (), None) + CID_UNDO = (0x107, TID_COMMAND, "Undo", "&Undo", [wx.ART_UNDO], (wx.ACCEL_CTRL, ord("Z"))) + CID_REDO = (0x108, TID_COMMAND, "Redo", "&Redo", [wx.ART_REDO], (wx.ACCEL_CTRL, ord("Y"))) + CID_CUT = (0x109, TID_COMMAND, "Cut", "Cu&t", [wx.ART_CUT], None) + CID_COPY = (0x10a, TID_COMMAND, "Copy", "&Copy", [wx.ART_COPY], None) + CID_PASTE = (0x10b, TID_COMMAND, "Paste", "&Paste", [wx.ART_PASTE], None) + CID_DELETE = (0x10c, TID_COMMAND, "Delete", "De&lete", [wx.ART_DELETE], None) + CID_INCRBRUSH = (0x10d, TID_COMMAND, "Increase brush size", "&Increase brush size", [wx.ART_PLUS], None) + CID_DECRBRUSH = (0x10e, TID_COMMAND, "Decrease brush size", "&Decrease brush size", [wx.ART_MINUS], None) + CID_SOLIDBRUSH = (0x10f, TID_COMMAND, "Solid brush", "&Solid brush", [None], None) + CID_RECT = (0x110, TID_COMMAND, "Rectangle", "&Rectangle", [None], None) + CID_CIRCLE = (0x111, TID_COMMAND, "Circle", "&Circle", [None], None) + CID_LINE = (0x112, TID_COMMAND, "Line", "&Line", [None], None) + CID_COLOUR00 = (0x113, TID_COMMAND, "Colour #00", "Colour #00", MiRCARTColours[0], None) + CID_COLOUR01 = (0x114, TID_COMMAND, "Colour #01", "Colour #01", MiRCARTColours[1], None) + CID_COLOUR02 = (0x115, TID_COMMAND, "Colour #02", "Colour #02", MiRCARTColours[2], None) + CID_COLOUR03 = (0x116, TID_COMMAND, "Colour #03", "Colour #03", MiRCARTColours[3], None) + CID_COLOUR04 = (0x117, TID_COMMAND, "Colour #04", "Colour #04", MiRCARTColours[4], None) + CID_COLOUR05 = (0x118, TID_COMMAND, "Colour #05", "Colour #05", MiRCARTColours[5], None) + CID_COLOUR06 = (0x119, TID_COMMAND, "Colour #06", "Colour #06", MiRCARTColours[6], None) + CID_COLOUR07 = (0x11a, TID_COMMAND, "Colour #07", "Colour #07", MiRCARTColours[7], None) + CID_COLOUR08 = (0x11b, TID_COMMAND, "Colour #08", "Colour #08", MiRCARTColours[8], None) + CID_COLOUR09 = (0x11c, TID_COMMAND, "Colour #09", "Colour #09", MiRCARTColours[9], None) + CID_COLOUR10 = (0x11d, TID_COMMAND, "Colour #10", "Colour #10", MiRCARTColours[10], None) + CID_COLOUR11 = (0x11e, TID_COMMAND, "Colour #11", "Colour #11", MiRCARTColours[11], None) + CID_COLOUR12 = (0x11f, TID_COMMAND, "Colour #12", "Colour #12", MiRCARTColours[12], None) + CID_COLOUR13 = (0x120, TID_COMMAND, "Colour #13", "Colour #13", MiRCARTColours[13], None) + CID_COLOUR14 = (0x121, TID_COMMAND, "Colour #14", "Colour #14", MiRCARTColours[14], None) + CID_COLOUR15 = (0x122, TID_COMMAND, "Colour #15", "Colour #15", MiRCARTColours[15], None) + # }}} + # {{{ Non-items + NID_MENU_SEP = (0x200, TID_NOTHING) + NID_TOOLBAR_SEP = (0x201, TID_NOTHING) + # }}} + # {{{ Menus + MID_FILE = (0x300, TID_MENU, "File", "&File", ( \ + CID_NEW, CID_OPEN, CID_SAVE, CID_SAVEAS, NID_MENU_SEP, \ + CID_EXPORT_PASTEBIN, CID_EXPORT_AS_PNG, NID_MENU_SEP, \ + CID_EXIT)) + MID_EDIT = (0x301, TID_MENU, "Edit", "&Edit", ( \ + CID_UNDO, CID_REDO, NID_MENU_SEP, \ + CID_CUT, CID_COPY, CID_PASTE, CID_DELETE, NID_MENU_SEP, \ + CID_INCRBRUSH, CID_DECRBRUSH, CID_SOLIDBRUSH)) + MID_TOOLS = (0x302, TID_MENU, "Tools", "&Tools", ( \ + CID_RECT, CID_CIRCLE, CID_LINE)) + # }}} + # {{{ Toolbars + BID_TOOLBAR = (0x400, TID_TOOLBAR, ( \ + CID_NEW, CID_OPEN, CID_SAVE, CID_SAVEAS, NID_TOOLBAR_SEP, \ + CID_UNDO, CID_REDO, NID_TOOLBAR_SEP, \ + CID_CUT, CID_COPY, CID_PASTE, CID_DELETE, NID_TOOLBAR_SEP, \ + CID_INCRBRUSH, CID_DECRBRUSH, CID_SOLIDBRUSH, NID_TOOLBAR_SEP, \ + CID_RECT, CID_CIRCLE, CID_LINE, NID_TOOLBAR_SEP, \ + CID_COLOUR00, CID_COLOUR01, CID_COLOUR02, CID_COLOUR03, CID_COLOUR04, \ + CID_COLOUR05, CID_COLOUR06, CID_COLOUR07, CID_COLOUR08, CID_COLOUR09, \ + CID_COLOUR10, CID_COLOUR11, CID_COLOUR12, CID_COLOUR13, CID_COLOUR14, \ + CID_COLOUR15)) + # }}} + # {{{ Accelerators (hotkeys) + AID_EDIT = (0x500, TID_ACCELS, (CID_UNDO, CID_REDO)) + # }}} + + # {{{ _drawIcon(self, solidColour): XXX + def _drawIcon(self, solidColour): + iconBitmap = wx.Bitmap((16,16)) + iconDc = wx.MemoryDC(); iconDc.SelectObject(iconBitmap); + iconBrush = wx.Brush(wx.Colour(solidColour), wx.BRUSHSTYLE_SOLID) + iconDc.SetBrush(iconBrush); iconDc.SetBackground(iconBrush); + iconDc.SetPen(wx.Pen(wx.Colour(solidColour), 1)) + iconDc.DrawRectangle(0, 0, 16, 16) + return iconBitmap + # }}} + # {{{ _initAccelTable(self, accelsDescr, handler): XXX + def _initAccelTable(self, accelsDescr, handler): + accelTableEntries = [wx.AcceleratorEntry() for n in range(0, len(accelsDescr[2]))] + for numAccel in range(0, len(accelsDescr[2])): + accelDescr = accelsDescr[2][numAccel] + if accelDescr[5] != None: + accelTableEntries[numAccel].Set(accelDescr[5][0], accelDescr[5][1], accelDescr[0]) + self.Bind(wx.EVT_MENU, handler, id=accelDescr[0]) + return accelTableEntries + # }}} + # {{{ _initMenus(self, menuBar, menusDescr, handler): XXX + def _initMenus(self, menuBar, menusDescr, handler): + for menuDescr in menusDescr: + menuWindow = wx.Menu() + for menuItem in menuDescr[4]: + if menuItem == self.NID_MENU_SEP: + menuWindow.AppendSeparator() + else: + menuItemWindow = menuWindow.Append(menuItem[0], menuItem[3], menuItem[2]) + self.menuItemsById[menuItem[0]] = menuItemWindow + self.Bind(wx.EVT_MENU, handler, menuItemWindow) + menuBar.Append(menuWindow, menuDescr[3]) + # }}} + # {{{ _initToolBars(self, toolBar, toolBarsDescr, handler): XXX + def _initToolBars(self, toolBar, toolBarsDescr, handler): + for toolBarDescr in toolBarsDescr: + for toolBarItem in toolBarDescr[2]: + if toolBarItem == self.NID_TOOLBAR_SEP: + toolBar.AddSeparator() + else: + if len(toolBarItem[4]) == 4: + toolBarItemIcon = self._drawIcon(toolBarItem[4]) + elif len(toolBarItem[4]) == 1 \ + and toolBarItem[4][0] != None: + toolBarItemIcon = wx.ArtProvider.GetBitmap( \ + toolBarItem[4][0], wx.ART_TOOLBAR, (16,16)) + else: + toolBarItemIcon = wx.ArtProvider.GetBitmap( \ + wx.ART_HELP, wx.ART_TOOLBAR, (16,16)) + toolBarItemWindow = self.toolBar.AddTool( \ + toolBarItem[0], toolBarItem[2], toolBarItemIcon) + self.Bind(wx.EVT_TOOL, handler, toolBarItemWindow) + self.Bind(wx.EVT_TOOL_RCLICKED, handler, toolBarItemWindow) + # }}} + # {{{ _updateStatusBar(self): XXX + def _updateStatusBar(self): + text = "Foreground colour:" + text += " " + str(self.panelCanvas.mircFg) + text += " | " + text += "Background colour:" + text += " " + str(self.panelCanvas.mircBg) + self.statusBar.SetStatusText(text) + # }}} + + # {{{ canvasExportAsPng(self): XXX + def canvasExportAsPng(self): + with wx.FileDialog(self, self.CID_SAVEAS[2], os.getcwd(), "", \ + "*.png", wx.FD_SAVE|wx.FD_OVERWRITE_PROMPT) as dialog: + if dialog.ShowModal() == wx.ID_CANCEL: + return False + else: + outPathName = dialog.GetPath() + outTmpFile = io.StringIO() + outToTextFile = MiRCARTToTextFile( \ + self.panelCanvas.canvasMap, self.canvasSize) + outToTextFile.export(outTmpFile) + MiRCARTToPngFile(tmpFile).export(outPathName) + return True + # }}} + # {{{ canvasExportPastebin(self): XXX + def canvasExportPastebin(self): + MiRCARTToPastebin("", \ + self.panelCanvas.canvasMap, self.canvasSize).export() + # }}} + # {{{ canvasNew(self, canvasPos=None, canvasSize=None, cellSize=None): XXX + def canvasNew(self, canvasPos=None, canvasSize=None, cellSize=None): + canvasPos = canvasPos if canvasPos != None else self.canvasPos + canvasSize = canvasSize if canvasSize != None else self.canvasSize + cellSize = cellSize if cellSize != None else self.cellSize + if self.panelCanvas != None: + self.panelCanvas.Close(); self.panelCanvas = None; + self.canvasPos = canvasPos; self.canvasSize = canvasSize; + self.cellSize = cellSize + self.panelCanvas = MiRCARTCanvas(self.panelSkin, parentFrame=self, \ + canvasPos=self.canvasPos, cellSize=self.cellSize, \ + canvasSize=self.canvasSize, canvasTools=self.canvasTools) + self._updateStatusBar(); self.onCanvasUpdate(); + # }}} + # {{{ canvasOpen(self): XXX + def canvasOpen(self): + with wx.FileDialog(self, self.CID_OPEN[2], os.getcwd(), "", \ + "*.txt", wx.FD_OPEN) as dialog: + if dialog.ShowModal() == wx.ID_CANCEL: + return False + else: + self.canvasPathName = dialog.GetPath() + with open(self.canvasPathName, "r") as newFile: + newFromTextFile = MiRCARTFromTextFile(newFile) + newMap = newFromTextFile.getMap() + self.canvasNew(canvasSize=newFromTextFile.getSize()) + eventDc = wx.ClientDC(self); tmpDc = wx.MemoryDC(); + tmpDc.SelectObject(self.panelCanvas.canvasBitmap) + for newNumRow in range(0, len(newMap)): + for newNumCol in range(0, len(newMap[newNumRow])): + self.panelCanvas.onJournalUpdate(False, \ + (newNumCol, newNumRow), \ + [newNumCol, newNumRow, \ + newMap[newNumRow][newNumCol][0][0], \ + newMap[newNumRow][newNumCol][0][1], \ + newMap[newNumRow][newNumCol][2]], \ + eventDc, tmpDc, (0, 0)) + wx.SafeYield() + return True + # }}} + # {{{ canvasSave(self): XXX + def canvasSave(self): + if self.canvasPathName == None: + if self.canvasSaveAs() == False: + return + try: + with open(self.canvasPathName, "w") as outFile: + MiRCARTToTextFile(self.panelCanvas.canvasMap, \ + self.panelCanvas.canvasSize).export(outFile) + except IOError as error: + pass + # }}} + # {{{ canvasSaveAs(self): XXX + def canvasSaveAs(self): + with wx.FileDialog(self, self.CID_SAVEAS[2], os.getcwd(), "", \ + "*.txt", wx.FD_SAVE|wx.FD_OVERWRITE_PROMPT) as dialog: + if dialog.ShowModal() == wx.ID_CANCEL: + return False + else: + self.canvasPathName = dialog.GetPath() + return True + # }}} + # {{{ onCanvasUpdate(self): XXX + def onCanvasUpdate(self): + if self.panelCanvas.canvasJournal.patchesUndo[self.panelCanvas.canvasJournal.patchesUndoLevel] != None: + self.menuItemsById[self.CID_UNDO[0]].Enable(True) + else: + self.menuItemsById[self.CID_UNDO[0]].Enable(False) + if self.panelCanvas.canvasJournal.patchesUndoLevel > 0: + self.menuItemsById[self.CID_REDO[0]].Enable(True) + else: + self.menuItemsById[self.CID_REDO[0]].Enable(False) + # }}} + # {{{ onClose(self, event): XXX + def onClose(self, event): + self.Destroy(); self.__del__(); + # }}} + # {{{ onFrameCommand(self, event): XXX + def onFrameCommand(self, event): + cid = event.GetId() + if cid == self.CID_NEW[0]: + self.canvasNew() + elif cid == self.CID_OPEN[0]: + self.canvasOpen() + elif cid == self.CID_SAVE[0]: + self.canvasSave() + elif cid == self.CID_SAVEAS[0]: + self.canvasSaveAs() + elif cid == self.CID_EXPORT_AS_PNG[0]: + self.canvasExportAsPng() + elif cid == self.CID_EXPORT_PASTEBIN[0]: + self.canvasExportPastebin() + elif cid == self.CID_EXIT[0]: + self.Close(True) + elif cid == self.CID_UNDO[0]: + self.panelCanvas.undo() + elif cid == self.CID_REDO[0]: + self.panelCanvas.redo() + elif cid == self.CID_CUT[0]: + pass + elif cid == self.CID_COPY[0]: + pass + elif cid == self.CID_PASTE[0]: + pass + elif cid == self.CID_DELETE[0]: + pass + elif cid == self.CID_INCRBRUSH[0]: + pass + elif cid == self.CID_DECRBRUSH[0]: + pass + elif cid == self.CID_SOLIDBRUSH[0]: + pass + elif cid == self.CID_RECT[0]: + pass + elif cid == self.CID_CIRCLE[0]: + pass + elif cid == self.CID_LINE[0]: + pass + elif cid >= self.CID_COLOUR00[0] \ + and cid <= self.CID_COLOUR15[0]: + numColour = cid - self.CID_COLOUR00[0] + if event.GetEventType() == wx.wxEVT_TOOL: + self.panelCanvas.mircFg = numColour + elif event.GetEventType() == wx.wxEVT_TOOL_RCLICKED: + self.panelCanvas.mircBg = numColour + self._updateStatusBar() + # }}} + # {{{ __del__(self): destructor method + def __del__(self): + if self.panelCanvas != None: + self.panelCanvas.Close(); self.panelCanvas = None; + # }}} + + # + # __init__(self, parent, appSize=(800, 600), canvasPos=(25, 50), cellSize=(7, 14), canvasSize=(100, 30), canvasTools=[]): initialisation method + def __init__(self, parent, appSize=(800, 600), canvasPos=(25, 50), cellSize=(7, 14), canvasSize=(100, 30), canvasTools=[]): + super().__init__(parent, wx.ID_ANY, "MiRCART", size=appSize) + self.panelSkin = wx.Panel(self, wx.ID_ANY) + self.canvasPathName = None + + self.menuItemsById = {}; self.menuBar = wx.MenuBar(); + self._initMenus(self.menuBar, \ + [self.MID_FILE, self.MID_EDIT, self.MID_TOOLS], self.onFrameCommand) + self.SetMenuBar(self.menuBar) + if not haveMiRCARTToPastebin: + self.menuItemsById[self.CID_EXPORT_PASTEBIN[0]].Enable(False) + if not haveMiRCARTToPngFile: + self.menuItemsById[self.CID_EXPORT_AS_PNG[0]].Enable(False) + + self.toolBar = wx.ToolBar(self.panelSkin, -1, \ + style=wx.HORIZONTAL|wx.TB_FLAT|wx.TB_NODIVIDER) + self.toolBar.SetToolBitmapSize((16,16)) + self._initToolBars(self.toolBar, [self.BID_TOOLBAR], self.onFrameCommand) + self.toolBar.Realize(); self.toolBar.Fit(); + + self.accelTable = wx.AcceleratorTable( \ + self._initAccelTable(self.AID_EDIT, self.onFrameCommand)) + self.SetAcceleratorTable(self.accelTable) + + self.Bind(wx.EVT_CLOSE, self.onClose) + self.statusBar = self.CreateStatusBar(); + self.SetFocus(); self.Show(True); + self.canvasTools = canvasTools + self.canvasNew(canvasPos, canvasSize, cellSize) + +# vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/MiRCARTFromTextFile.py b/MiRCARTFromTextFile.py new file mode 100644 index 0000000..6459877 --- /dev/null +++ b/MiRCARTFromTextFile.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +# +# MiRCARTFromTextFile.py -- XXX +# Copyright (c) 2018 Lucio Andrés Illanes Albornoz +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +class MiRCARTFromTextFile(): + """XXX""" + inFile = inSize = outMap = None + + # + # _CellState(): Cell state + class _CellState(): + CS_NONE = 0x00 + CS_BOLD = 0x01 + CS_ITALIC = 0x02 + CS_UNDERLINE = 0x04 + + # + # _ParseState(): Parsing loop state + class _ParseState(): + PS_CHAR = 1 + PS_COLOUR_DIGIT0 = 2 + PS_COLOUR_DIGIT1 = 3 + + # {{{ _parseCharAsColourSpec(self, colourSpec, curColours): XXX + def _parseCharAsColourSpec(self, colourSpec, curColours): + if len(colourSpec) > 0: + colourSpec = colourSpec.split(",") + if len(colourSpec) == 2: + return (int(colourSpec[0] or curColours[0]), \ + int(colourSpec[1])) + elif len(colourSpec) == 1: + return (int(colourSpec[0]), curColours[0]) + else: + return (15, 1) + # }}} + # {{{ _flipCellStateBit(self, cellState, bit): XXX + def _flipCellStateBit(self, cellState, bit): + if cellState & bit: + return cellState & ~bit + else: + return cellState | bit + # }}} + # {{{ _transform(self): XXX + def _transform(self): + inCurColourSpec = ""; inCurRow = -1; + inLine = self.inFile.readline() + inSize = [0, 0]; outMap = []; inMaxCols = 0; + while inLine: + inCellState = self._CellState.CS_NONE + inParseState = self._ParseState.PS_CHAR + inCurCol = 0; inMaxCol = len(inLine); + inCurColourDigits = 0; inCurColours = (15, 1); inCurColourSpec = ""; + inCurRow += 1; outMap.append([]); inRowCols = 0; inSize[1] += 1; + while inCurCol < inMaxCol: + inChar = inLine[inCurCol] + if inChar in set("\r\n"): \ + inCurCol += 1 + elif inParseState == self._ParseState.PS_CHAR: + inCurCol += 1 + if inChar == "": + inCellState = self._flipCellStateBit( \ + inCellState, self._CellState.CS_BOLD) + elif inChar == "": + inParseState = self._ParseState.PS_COLOUR_DIGIT0 + elif inChar == "": + inCellState = self._flipCellStateBit( \ + inCellState, self._CellState.CS_ITALIC) + elif inChar == "": + inCellState |= self._CellState.CS_NONE + inCurColours = (15, 1) + elif inChar == "": + inCurColours = (inCurColours[1], inCurColours[0]) + elif inChar == "": + inCellState = self._flipCellStateBit( \ + inCellState, self._CellState.CS_UNDERLINE) + else: + inRowCols += 1 + outMap[inCurRow].append((inCurColours, inCellState, inChar)) + elif inParseState == self._ParseState.PS_COLOUR_DIGIT0 \ + or inParseState == self._ParseState.PS_COLOUR_DIGIT1: + if inChar == "," \ + and inParseState == self._ParseState.PS_COLOUR_DIGIT0: + inCurCol += 1 + inCurColourDigits = 0; inCurColourSpec += inChar; + inParseState = self._ParseState.PS_COLOUR_DIGIT1 + elif inChar in set("0123456789") \ + and inCurColourDigits == 0: + inCurCol += 1 + inCurColourDigits += 1; inCurColourSpec += inChar; + elif inChar in set("0123456789") \ + and inCurColourDigits == 1 \ + and inCurColourSpec[-1] == "0": + inCurCol += 1 + inCurColourDigits += 1; inCurColourSpec += inChar; + elif inChar in set("012345") \ + and inCurColourDigits == 1 \ + and inCurColourSpec[-1] == "1": + inCurCol += 1 + inCurColourDigits += 1; inCurColourSpec += inChar; + else: + inCurColours = self._parseCharAsColourSpec( \ + inCurColourSpec, inCurColours) + inCurColourDigits = 0; inCurColourSpec = ""; + inParseState = self._ParseState.PS_CHAR + inMaxCols = max(inMaxCols, inRowCols) + inLine = self.inFile.readline() + inSize[0] = inMaxCols; self.inSize = inSize; self.outMap = outMap; + # }}} + # {{{ getMap(self): XXX + def getMap(self): + if self.outMap == None: + self._transform() + return self.outMap + # }}} + # {{{ getSize(self): XXX + def getSize(self): + if self.inSize == None: + self._transform() + return self.inSize + # }}} + + # + # __init__(self): initialisation method + def __init__(self, inFile): + self.inFile = inFile; self.inSize = None; self.outMap = None; + +# vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/MiRCARTToPastebin.py b/MiRCARTToPastebin.py new file mode 100644 index 0000000..88b25bc --- /dev/null +++ b/MiRCARTToPastebin.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +# +# MiRCARTToPastebin.py -- XXX +# Copyright (c) 2018 Lucio Andrés Illanes Albornoz +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +from MiRCARTToTextFile import MiRCARTToTextFile +import io +import base64 +import requests, urllib.request + +class MiRCARTToPastebin(): + apiDevKey = outFile = outToTextFile = None + + # export(self): XXX + def export(self): + self.outToTextFile.export(self.outFile, pasteName="", pastePrivate=0) + requestData = { \ + "api_dev_key": self.apiDevKey, \ + "api_option": "paste", \ + "api_paste_code": base64.b64encode(self.outFile.read()), \ + "api_paste_name": pasteName, \ + "api_paste_private": pastePrivate} + responseHttp = requests.post("https://pastebin.com/post.php", \ + data=requestData) + if responseHttp.status_code == 200: + return responseHttp.text + else: + return None + + # __init__(self, canvasMap, canvasSize): XXX + def __init__(self, apiDevKey, canvasMap, canvasSize): + self.apiDevKey = apiDevKey + self.outFile = io.StringIO() + self.outToTextFile = MiRCARTToTextFile(canvasMap, canvasSize) + +# vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/MiRCARTToPngFile.py b/MiRCARTToPngFile.py new file mode 100755 index 0000000..8503b41 --- /dev/null +++ b/MiRCARTToPngFile.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +# +# MiRCARTToPngFile.py -- convert ASCII w/ mIRC control codes to monospaced PNG (for EFnet #MiRCART) +# Copyright (c) 2018 Lucio Andrés Illanes Albornoz +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +from PIL import Image, ImageDraw, ImageFont +from MiRCARTFromTextFile import MiRCARTFromTextFile +import string, sys + +class MiRCARTToPngFile: + """XXX""" + inFile = inFilePath = inFromTextFile = None + outFontFilePath = outFontSize = None + + # {{{ _ColourMapBold: mIRC colour number to RGBA map given ^B (bold) + _ColourMapBold = [ + [255, 255, 255], # White + [85, 85, 85], # Grey + [85, 85, 255], # Light Blue + [85, 255, 85], # Light Green + [255, 85, 85], # Light Red + [255, 85, 85], # Light Red + [255, 85, 255], # Pink + [255, 255, 85], # Light Yellow + [255, 255, 85], # Light Yellow + [85, 255, 85], # Light Green + [85, 255, 255], # Light Cyan + [85, 255, 255], # Light Cyan + [85, 85, 255], # Light Blue + [255, 85, 255], # Pink + [85, 85, 85], # Grey + [255, 255, 255], # White + ] + # }}} + # {{{ _ColourMapNormal: mIRC colour number to RGBA map given none of ^[BFV_] (bold, italic, reverse, underline) + _ColourMapNormal = [ + [255, 255, 255], # White + [0, 0, 0], # Black + [0, 0, 187], # Blue + [0, 187, 0], # Green + [255, 85, 85], # Light Red + [187, 0, 0], # Red + [187, 0, 187], # Purple + [187, 187, 0], # Yellow + [255, 255, 85], # Light Yellow + [85, 255, 85], # Light Green + [0, 187, 187], # Cyan + [85, 255, 255], # Light Cyan + [85, 85, 255], # Light Blue + [255, 85, 255], # Pink + [85, 85, 85], # Grey + [187, 187, 187], # Light Grey + ] + # }}} + # {{{ _drawUnderline(self, curPos, fontSize, imgDraw): XXX + def _drawUnderLine(self, curPos, fontSize, imgDraw): + imgDraw.line( \ + (curPos[0], \ + curPos[1] + (fontSize[1] - 2)), \ + (curPos[0] + fontSize[0], \ + curPos[1] + (fontSize[1] - 2)), fill=outColours[0]) + # }}} + # {{{ export(self, outFilePath): XXX + def export(self, outFilePath): + inSize = self.inFromTextFile.getSize(); + outSize = [inSize[n] * self.outImgFontSize[n] for n in range(0, 2)] + outCurPos = [0, 0] + outImg = Image.new("RGBA", outSize, (*self._ColourMapNormal[1], 255)) + outImgDraw = ImageDraw.Draw(outImg) + for inCurRow in range(0, inSize[1]): + for inCurCol in range(0, len(self.inFromTextFile.outMap[inCurRow])): + inCurCell = self.inFromTextFile.outMap[inCurRow][inCurCol] + outColours = [0, 0] + if inCurCell[1] & MiRCARTFromTextFile._CellState.CS_BOLD: + if inCurCell[2] != " ": + outColours[0] = self._ColourMapBold[inCurCell[0][0]] + outColours[1] = self._ColourMapBold[inCurCell[0][1]] + else: + if inCurCell[2] != " ": + outColours[0] = self._ColourMapNormal[inCurCell[0][0]] + outColours[1] = self._ColourMapNormal[inCurCell[0][1]] + outImgDraw.rectangle((*outCurPos, \ + outCurPos[0] + self.outImgFontSize[0], \ + outCurPos[1] + self.outImgFontSize[1]), \ + fill=(*outColours[1], 255)) + if inCurCell[2] != " " \ + and outColours[0] != outColours[1]: + # XXX implement italic + outImgDraw.text(outCurPos, \ + inCurCell[2], (*outColours[0], 255), self.outImgFont) + if inCurCell[1] & MiRCARTFromTextFile._CellState.CS_UNDERLINE: + self._drawUnderLine(curPos, self.outImgFontSize, outImgDraw) + outCurPos[0] += self.outImgFontSize[0]; + outCurPos[0] = 0 + outCurPos[1] += self.outImgFontSize[1] + outImg.save(outFilePath); + # }}} + + # + # __init__(self, inFilePath, fontFilePath="DejaVuSansMono.ttf", fontSize=11): initialisation method + def __init__(self, inFilePath, fontFilePath="DejaVuSansMono.ttf", fontSize=11): + self.inFilePath = inFilePath; self.inFile = open(inFilePath, "r"); + self.inFromTextFile = MiRCARTFromTextFile(self.inFile) + + self.outFontFilePath = fontFilePath; self.outFontSize = int(fontSize); + self.outImgFont = ImageFont.truetype( \ + self.outFontFilePath, self.outFontSize) + self.outImgFontSize = [*self.outImgFont.getsize(" ")] + self.outImgFontSize[1] += 3 + +# +# Entry point +def main(*argv): + MiRCARTToPngFile(argv[1], *argv[3:]).export(argv[2]) +if __name__ == "__main__": + if ((len(sys.argv) - 1) < 2)\ + or ((len(sys.argv) - 1) > 4): + print("usage: {} " \ + " " \ + " " \ + "[] " \ + "[]".format(sys.argv[0]), file=sys.stderr) + else: + main(*sys.argv) + +# vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/MiRCARTToTextFile.py b/MiRCARTToTextFile.py new file mode 100644 index 0000000..ee0950d --- /dev/null +++ b/MiRCARTToTextFile.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +# +# MiRCARTToTextFile.py -- XXX +# Copyright (c) 2018 Lucio Andrés Illanes Albornoz +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +class MiRCARTToTextFile(): + canvasMap = canvasSize = None + + # export(self, outFile): XXX + def export(self, outFile): + for canvasRow in range(0, self.canvasSize[1]): + canvasLastColours = [] + for canvasCol in range(0, self.canvasSize[0]): + canvasColColours = self.canvasMap[canvasRow][canvasCol][0:2] + canvasColText = self.canvasMap[canvasRow][canvasCol][2] + if canvasColColours != canvasLastColours: + canvasLastColours = canvasColColours + outFile.write("\x03" + \ + str(canvasColColours[0]) + \ + "," + str(canvasColColours[1])) + outFile.write(canvasColText) + outFile.write("\n") + + # __init__(self, canvasMap, canvasSize): XXX + def __init__(self, canvasMap, canvasSize): + self.canvasMap = canvasMap; self.canvasSize = canvasSize; + +# vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/MiRCARTTool.py b/MiRCARTTool.py new file mode 100644 index 0000000..0dad5ad --- /dev/null +++ b/MiRCARTTool.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +# +# MiRCARTTool.py -- XXX +# Copyright (c) 2018 Lucio Andrés Illanes Albornoz +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +class MiRCARTTool(): + """XXX""" + parentCanvas = None + + # {{{ onMouseEvent(self, event, mapPoint, isDragging, isLeftDown, isRightDown): XXX + def onMouseEvent(self, event, mapPoint, isDragging, isLeftDown, isRightDown): + pass + # }}} + + # + # __init__(self, parentCanvas): initialisation method + def __init__(self, parentCanvas): + self.parentCanvas = parentCanvas + +# vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/MiRCARTToolRect.py b/MiRCARTToolRect.py new file mode 100644 index 0000000..b9e9fb2 --- /dev/null +++ b/MiRCARTToolRect.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# +# MiRCARTToolRect.py -- XXX +# Copyright (c) 2018 Lucio Andrés Illanes Albornoz +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +from MiRCARTTool import MiRCARTTool + +class MiRCARTToolRect(MiRCARTTool): + """XXX""" + + # + # onMouseEvent(self, event, mapPoint, isDragging, isLeftDown, isRightDown): XXX + def onMouseEvent(self, event, mapPoint, isDragging, isLeftDown, isRightDown): + if isLeftDown: + return [[False, [[0, 0, \ + self.parentCanvas.mircFg, \ + self.parentCanvas.mircFg, " "]]], + [True, [[0, 0, \ + self.parentCanvas.mircFg, \ + self.parentCanvas.mircFg, " "]]]] + elif isRightDown: + return [[False, [[0, 0, \ + self.parentCanvas.mircBg, \ + self.parentCanvas.mircBg, " "]]], \ + [True, [[0, 0, \ + self.parentCanvas.mircBg, \ + self.parentCanvas.mircBg, " "]]]] + else: + return [[True, [[0, 0, \ + self.parentCanvas.mircFg, \ + self.parentCanvas.mircFg, " "]]]] + +# vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/README.md b/README.md index 736ccee..5785840 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ * Prerequisites: python3 && python3-{json,requests,urllib3} on Debian-family Linux distributions * IrcMiRCARTBot.py usage: IrcMiRCARTBot.py `` [``] [``] [``] [``] [``] -# MiRC2png.py -- convert ASCII w/ mIRC control codes to monospaced PNG (pending cleanup) +# MiRCARTToPngFile.py -- convert ASCII w/ mIRC control codes to monospaced PNG (pending cleanup) * Prerequisites: python3 && python3-pil on Debian-family Linux distributions * MiRC2png.py usage: MiRC2png.py `` `` [``] [``] From 3c28fa007110b63ded1c509d27365c34f03fa0aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Sun, 7 Jan 2018 04:19:38 +0100 Subject: [PATCH 073/148] MiRCARTCanvasStore.py: split & merge MiRCART{FromTextFile,ToPastebin,ToTextFile}.py. --- MiRCARTCanvas.py | 65 ++++++--- MiRCARTCanvasJournal.py | 9 +- ...RTFromTextFile.py => MiRCARTCanvasStore.py | 132 +++++++++++++++--- MiRCARTFrame.py | 77 ++++------ MiRCARTToPastebin.py | 55 -------- MiRCARTToTextFile.py | 47 ------- 6 files changed, 193 insertions(+), 192 deletions(-) rename MiRCARTFromTextFile.py => MiRCARTCanvasStore.py (54%) delete mode 100644 MiRCARTToPastebin.py delete mode 100644 MiRCARTToTextFile.py diff --git a/MiRCARTCanvas.py b/MiRCARTCanvas.py index 3743b5c..f8b5a47 100644 --- a/MiRCARTCanvas.py +++ b/MiRCARTCanvas.py @@ -23,6 +23,7 @@ # from MiRCARTCanvasJournal import MiRCARTCanvasJournal +from MiRCARTCanvasStore import MiRCARTCanvasStore from MiRCARTColours import MiRCARTColours import wx @@ -32,8 +33,18 @@ class MiRCARTCanvas(wx.Panel): canvasPos = canvasSize = canvasWinSize = cellPos = cellSize = None canvasBitmap = canvasMap = canvasTools = None mircBg = mircFg = mircBrushes = mircPens = None - canvasJournal = None + canvasJournal = canvasStore = None + # {{{ _initBrushesAndPens(self): XXX + def _initBrushesAndPens(self): + self.mircBrushes = [None for x in range(len(MiRCARTColours))] + self.mircPens = [None for x in range(len(MiRCARTColours))] + for mircColour in range(0, len(MiRCARTColours)): + self.mircBrushes[mircColour] = wx.Brush( \ + wx.Colour(MiRCARTColours[mircColour]), wx.BRUSHSTYLE_SOLID) + self.mircPens[mircColour] = wx.Pen( \ + wx.Colour(MiRCARTColours[mircColour]), 1) + # }}} # {{{ _drawPatch(self, patch, eventDc, tmpDc, atPoint): XXX def _drawPatch(self, patch, eventDc, tmpDc, atPoint): absPoint = self._relMapPointToAbsPoint((patch[0], patch[1]), atPoint) @@ -69,6 +80,7 @@ class MiRCARTCanvas(wx.Panel): def _setMapCell(self, absMapPoint, colourFg, colourBg, char): self.canvasMap[absMapPoint[1]][absMapPoint[0]] = [colourFg, colourBg, char] # }}} + # {{{ onClose(self, event): XXX def onClose(self, event): self.Destroy(); self.__del__(); @@ -106,10 +118,30 @@ class MiRCARTCanvas(wx.Panel): def redo(self): return self.canvasJournal.redo() # }}} + # {{{ resize(self, newCanvasSize): XXX + def resize(self, newCanvasSize): + if newCanvasSize != self.canvasSize: + self.SetSize(*self.canvasPos, \ + newCanvasSize[0] * self.cellSize[0], \ + newCanvasSize[1] * self.cellSize[1]) + for numRow in range(0, self.canvasSize[1]): + for numNewCol in range(self.canvasSize[0], newCanvasSize[0]): + self.canvasMap[numRow].append([1, 1, " "]) + for numNewRow in range(self.canvasSize[1], newCanvasSize[1]): + self.canvasMap.append([]) + for numNewCol in range(0, newCanvasSize[0]): + self.canvasMap[numNewRow].append([1, 1, " "]) + self.canvasSize = newCanvasSize + canvasWinSize = ( \ + self.cellSize[0] * self.canvasSize[0], \ + self.cellSize[1] * self.canvasSize[1]) + self.canvasBitmap = wx.Bitmap(canvasWinSize) + # }}} # {{{ undo(self): XXX def undo(self): return self.canvasJournal.undo() # }}} + # {{{ __del__(self): destructor method def __del__(self): if self.canvasBitmap != None: @@ -124,30 +156,19 @@ class MiRCARTCanvas(wx.Panel): # # _init__(self, parent, parentFrame, canvasPos, cellSize, canvasSize, canvasTools): initialisation method - def __init__(self, parent, parentFrame, canvasPos, cellSize, canvasSize, canvasTools): + def __init__(self, parent, parentFrame, canvasPos, canvasSize, canvasTools, cellSize): self.parentFrame = parentFrame - canvasWinSize = (cellSize[0] * canvasSize[0], cellSize[1] * canvasSize[1]) - super().__init__(parent, pos=canvasPos, size=canvasWinSize) - self.canvasPos = canvasPos; self.canvasSize = canvasSize; self.canvasWinSize = canvasWinSize; - self.cellPos = (0, 0); self.cellSize = cellSize; + self.canvasPos = canvasPos; self.canvasSize = canvasSize; + self.canvasTools = [canvasTool(self) for canvasTool in canvasTools] + self.cellSize = cellSize - self.canvasBitmap = wx.Bitmap(canvasWinSize) - self.canvasMap = [[[1, 1, " "] for x in range(canvasSize[0])] for y in range(canvasSize[1])] - self.canvasTools = [] - for canvasTool in canvasTools: - self.canvasTools.append(canvasTool(self)) - - self.mircBg = 1; self.mircFg = 4; - self.mircBrushes = [None for x in range(len(MiRCARTColours))] - self.mircPens = [None for x in range(len(MiRCARTColours))] - for mircColour in range(0, len(MiRCARTColours)): - self.mircBrushes[mircColour] = wx.Brush( \ - wx.Colour(MiRCARTColours[mircColour]), wx.BRUSHSTYLE_SOLID) - self.mircPens[mircColour] = wx.Pen( \ - wx.Colour(MiRCARTColours[mircColour]), 1) - - self.canvasJournal = MiRCARTCanvasJournal(self) + self.cellPos = (0, 0) + self.mircBg = 1; self.mircFg = 4; self._initBrushesAndPens(); + self.canvasJournal = MiRCARTCanvasJournal(parentCanvas=self) + self.canvasStore = MiRCARTCanvasStore(parentCanvas=self) + super().__init__(parent, pos=canvasPos, \ + size=[w*h for w,h in zip(canvasSize, cellSize)]) self.Bind(wx.EVT_CLOSE, self.onClose) self.Bind(wx.EVT_LEFT_DOWN, self.onMouseEvent) self.Bind(wx.EVT_MOTION, self.onMouseEvent) diff --git a/MiRCARTCanvasJournal.py b/MiRCARTCanvasJournal.py index 03e5f93..a712d36 100644 --- a/MiRCARTCanvasJournal.py +++ b/MiRCARTCanvasJournal.py @@ -83,6 +83,11 @@ class MiRCARTCanvasJournal(): else: return False # }}} + # {{{ reset(self): XXX + def reset(self): + self.patchesTmp = [] + self.patchesUndo = [None]; self.patchesUndoLevel = 0; + # }}} # {{{ undo(self): XXX def undo(self): if self.patchesUndo[self.patchesUndoLevel] != None: @@ -98,8 +103,6 @@ class MiRCARTCanvasJournal(): # # __init__(self, parentCanvas): initialisation method def __init__(self, parentCanvas): - self.parentCanvas = parentCanvas - self.patchesTmp = [] - self.patchesUndo = [None]; self.patchesUndoLevel = 0; + self.parentCanvas = parentCanvas; self.reset(); # vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/MiRCARTFromTextFile.py b/MiRCARTCanvasStore.py similarity index 54% rename from MiRCARTFromTextFile.py rename to MiRCARTCanvasStore.py index 6459877..a3b106e 100644 --- a/MiRCARTFromTextFile.py +++ b/MiRCARTCanvasStore.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# MiRCARTFromTextFile.py -- XXX +# MiRCARTCanvasStore.py -- XXX # Copyright (c) 2018 Lucio Andrés Illanes Albornoz # # Permission is hereby granted, free of charge, to any person obtaining a copy @@ -22,9 +22,26 @@ # SOFTWARE. # -class MiRCARTFromTextFile(): +import base64 +import io +import wx + +try: + from MiRCARTToPngFile import MiRCARTToPngFile + haveMiRCARTToPngFile = True +except ImportError: + haveMiRCARTToPngFile = False + +try: + import requests, urllib.request + haveUrllib = True +except ImportError: + haveUrllib = False + +class MiRCARTCanvasStore(): """XXX""" inFile = inSize = outMap = None + parentCanvas = None # # _CellState(): Cell state @@ -60,8 +77,72 @@ class MiRCARTFromTextFile(): else: return cellState | bit # }}} - # {{{ _transform(self): XXX - def _transform(self): + + # {{{ exportPastebin(self, apiDevKey): XXX + def exportPastebin(self, apiDevKey): + if haveUrllib: + outFile = io.StringIO(); self.exportTextFile(outFile); + requestData = { \ + "api_dev_key": self.apiDevKey, \ + "api_option": "paste", \ + "api_paste_code": base64.b64encode(outFile.read()), \ + "api_paste_name": pasteName, \ + "api_paste_private": pastePrivate} + responseHttp = requests.post("https://pastebin.com/post.php", \ + data=requestData) + if responseHttp.status_code == 200: + return responseHttp.text + else: + return None + else: + return None + # }}} + # {{{ exportPngFile(self): XXX + def exportPngFile(self, pathName): + if haveMiRCARTToPngFile: + outFile = io.StringIO(); self.exportTextFile(outFile); + MiRCARTToPng(outFile).export(pathName) + return True + else: + return False + # }}} + # {{{ exportTextFile(self, outFile): XXX + def exportTextFile(self, outFile): + canvasMap = self.parentCanvas.canvasMap + canvasSize = self.parentCanvas.canvasSize + for canvasRow in range(0, canvasSize[1]): + canvasLastColours = [] + for canvasCol in range(0, canvasSize[0]): + canvasColColours = canvasMap[canvasRow][canvasCol][0:2] + canvasColText = self.canvasMap[canvasRow][canvasCol][2] + if canvasColColours != canvasLastColours: + canvasLastColours = canvasColColours + outFile.write("\x03" + \ + str(canvasColColours[0]) + \ + "," + str(canvasColColours[1])) + outFile.write(canvasColText) + outFile.write("\n") + # }}} + # {{{ importIntoPanel(self): XXX + def importIntoPanel(self): + canvasSize = self.inSize; self.parentCanvas.resize(canvasSize); + self.parentCanvas.canvasJournal.reset() + eventDc = wx.ClientDC(self.parentCanvas); tmpDc = wx.MemoryDC(); + tmpDc.SelectObject(self.parentCanvas.canvasBitmap) + for numRow in range(0, len(self.outMap)): + for numCol in range(0, len(self.outMap[numRow])): + self.parentCanvas.onJournalUpdate(False, \ + (numCol, numRow), [numCol, numRow, \ + self.outMap[numRow][numCol][0][0], \ + self.outMap[numRow][numCol][0][1], \ + self.outMap[numRow][numCol][2]], \ + eventDc, tmpDc, (0, 0)) + wx.SafeYield() + # }}} + # {{{ importTextFile(self, pathName): XXX + def importTextFile(self, pathName): + self.inFile = open(pathName, "r") + self.inSize = self.outMap = None; inCurColourSpec = ""; inCurRow = -1; inLine = self.inFile.readline() inSize = [0, 0]; outMap = []; inMaxCols = 0; @@ -125,23 +206,38 @@ class MiRCARTFromTextFile(): inMaxCols = max(inMaxCols, inRowCols) inLine = self.inFile.readline() inSize[0] = inMaxCols; self.inSize = inSize; self.outMap = outMap; + self.inFile.close() # }}} - # {{{ getMap(self): XXX - def getMap(self): - if self.outMap == None: - self._transform() - return self.outMap - # }}} - # {{{ getSize(self): XXX - def getSize(self): - if self.inSize == None: - self._transform() - return self.inSize + # {{{ importNew(self, newCanvasSize=None): XXX + def importNew(self, newCanvasSize=None): + if newCanvasSize != None: + self.parentCanvas.resize(newCanvasSize) + self.parentCanvas.canvasJournal.reset() + self.parentCanvas.canvasMap = [[[1, 1, " "] \ + for x in range(self.parentCanvas.canvasSize[0])] \ + for y in range(self.parentCanvas.canvasSize[1])] + canvasWinSize = ( \ + self.parentCanvas.cellSize[0] * self.parentCanvas.canvasSize[0], \ + self.parentCanvas.cellSize[1] * self.parentCanvas.canvasSize[1]) + if self.parentCanvas.canvasBitmap != None: + self.parentCanvas.canvasBitmap.Destroy() + self.parentCanvas.canvasBitmap = wx.Bitmap(canvasWinSize) + eventDc = wx.ClientDC(self.parentCanvas); tmpDc = wx.MemoryDC(); + tmpDc.SelectObject(self.parentCanvas.canvasBitmap) + for numRow in range(0, len(self.parentCanvas.canvasMap)): + for numCol in range(0, len(self.parentCanvas.canvasMap[numRow])): + self.parentCanvas.onJournalUpdate(False, \ + (numCol, numRow), [numCol, numRow, 1, 1, " "], \ + eventDc, tmpDc, (0, 0)) + wx.SafeYield() # }}} # - # __init__(self): initialisation method - def __init__(self, inFile): - self.inFile = inFile; self.inSize = None; self.outMap = None; + # __init__(self, inFile=None, parentCanvas=None): initialisation method + def __init__(self, inFile=None, parentCanvas=None): + self.inFile = inFile; self.inSize = self.outMap = None; + self.parentCanvas = parentCanvas + if inFile != None: + self.importTextFile(inFile) # vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/MiRCARTFrame.py b/MiRCARTFrame.py index 8c8ae20..e60c41a 100644 --- a/MiRCARTFrame.py +++ b/MiRCARTFrame.py @@ -24,8 +24,6 @@ from MiRCARTCanvas import MiRCARTCanvas from MiRCARTColours import MiRCARTColours -from MiRCARTFromTextFile import MiRCARTFromTextFile -from MiRCARTToTextFile import MiRCARTToTextFile import os, wx try: @@ -194,31 +192,26 @@ class MiRCARTFrame(wx.Frame): if dialog.ShowModal() == wx.ID_CANCEL: return False else: - outPathName = dialog.GetPath() - outTmpFile = io.StringIO() - outToTextFile = MiRCARTToTextFile( \ - self.panelCanvas.canvasMap, self.canvasSize) - outToTextFile.export(outTmpFile) - MiRCARTToPngFile(tmpFile).export(outPathName) - return True + try: + outPathName = dialog.GetPath() + self.panelCanvas.exportPngFile(outhPathName) + return True + except IOError as error: + pass # }}} # {{{ canvasExportPastebin(self): XXX def canvasExportPastebin(self): - MiRCARTToPastebin("", \ - self.panelCanvas.canvasMap, self.canvasSize).export() + try: + self.panelCanvas.exportPastebin("") + return True + except IOError as error: + pass # }}} - # {{{ canvasNew(self, canvasPos=None, canvasSize=None, cellSize=None): XXX - def canvasNew(self, canvasPos=None, canvasSize=None, cellSize=None): - canvasPos = canvasPos if canvasPos != None else self.canvasPos - canvasSize = canvasSize if canvasSize != None else self.canvasSize - cellSize = cellSize if cellSize != None else self.cellSize - if self.panelCanvas != None: - self.panelCanvas.Close(); self.panelCanvas = None; - self.canvasPos = canvasPos; self.canvasSize = canvasSize; - self.cellSize = cellSize - self.panelCanvas = MiRCARTCanvas(self.panelSkin, parentFrame=self, \ - canvasPos=self.canvasPos, cellSize=self.cellSize, \ - canvasSize=self.canvasSize, canvasTools=self.canvasTools) + # {{{ canvasNew(self, newCanvasSize=None): XXX + def canvasNew(self, newCanvasSize=None): + if newCanvasSize == None: + newCanvasSize = (100, 30) + self.panelCanvas.canvasStore.importNew(newCanvasSize) self._updateStatusBar(); self.onCanvasUpdate(); # }}} # {{{ canvasOpen(self): XXX @@ -229,23 +222,10 @@ class MiRCARTFrame(wx.Frame): return False else: self.canvasPathName = dialog.GetPath() - with open(self.canvasPathName, "r") as newFile: - newFromTextFile = MiRCARTFromTextFile(newFile) - newMap = newFromTextFile.getMap() - self.canvasNew(canvasSize=newFromTextFile.getSize()) - eventDc = wx.ClientDC(self); tmpDc = wx.MemoryDC(); - tmpDc.SelectObject(self.panelCanvas.canvasBitmap) - for newNumRow in range(0, len(newMap)): - for newNumCol in range(0, len(newMap[newNumRow])): - self.panelCanvas.onJournalUpdate(False, \ - (newNumCol, newNumRow), \ - [newNumCol, newNumRow, \ - newMap[newNumRow][newNumCol][0][0], \ - newMap[newNumRow][newNumCol][0][1], \ - newMap[newNumRow][newNumCol][2]], \ - eventDc, tmpDc, (0, 0)) - wx.SafeYield() - return True + self.panelCanvas.canvasStore.importTextFile(self.canvasPathName) + self.panelCanvas.canvasStore.importIntoPanel() + self._updateStatusBar(); self.onCanvasUpdate(); + return True # }}} # {{{ canvasSave(self): XXX def canvasSave(self): @@ -253,9 +233,8 @@ class MiRCARTFrame(wx.Frame): if self.canvasSaveAs() == False: return try: - with open(self.canvasPathName, "w") as outFile: - MiRCARTToTextFile(self.panelCanvas.canvasMap, \ - self.panelCanvas.canvasSize).export(outFile) + self.panelCanvas.exportTextFile(self.canvasPathName) + return True except IOError as error: pass # }}} @@ -345,10 +324,11 @@ class MiRCARTFrame(wx.Frame): def __init__(self, parent, appSize=(800, 600), canvasPos=(25, 50), cellSize=(7, 14), canvasSize=(100, 30), canvasTools=[]): super().__init__(parent, wx.ID_ANY, "MiRCART", size=appSize) self.panelSkin = wx.Panel(self, wx.ID_ANY) + self.canvasPos = canvasPos; self.cellSize = cellSize; self.canvasSize = canvasSize; self.canvasPathName = None self.menuItemsById = {}; self.menuBar = wx.MenuBar(); - self._initMenus(self.menuBar, \ + self._initMenus(self.menuBar, \ [self.MID_FILE, self.MID_EDIT, self.MID_TOOLS], self.onFrameCommand) self.SetMenuBar(self.menuBar) if not haveMiRCARTToPastebin: @@ -356,13 +336,13 @@ class MiRCARTFrame(wx.Frame): if not haveMiRCARTToPngFile: self.menuItemsById[self.CID_EXPORT_AS_PNG[0]].Enable(False) - self.toolBar = wx.ToolBar(self.panelSkin, -1, \ + self.toolBar = wx.ToolBar(self.panelSkin, -1, \ style=wx.HORIZONTAL|wx.TB_FLAT|wx.TB_NODIVIDER) self.toolBar.SetToolBitmapSize((16,16)) self._initToolBars(self.toolBar, [self.BID_TOOLBAR], self.onFrameCommand) self.toolBar.Realize(); self.toolBar.Fit(); - self.accelTable = wx.AcceleratorTable( \ + self.accelTable = wx.AcceleratorTable( \ self._initAccelTable(self.AID_EDIT, self.onFrameCommand)) self.SetAcceleratorTable(self.accelTable) @@ -370,6 +350,9 @@ class MiRCARTFrame(wx.Frame): self.statusBar = self.CreateStatusBar(); self.SetFocus(); self.Show(True); self.canvasTools = canvasTools - self.canvasNew(canvasPos, canvasSize, cellSize) + self.panelCanvas = MiRCARTCanvas(self.panelSkin, parentFrame=self, \ + canvasPos=self.canvasPos, canvasSize=self.canvasSize, \ + canvasTools=self.canvasTools, cellSize=self.cellSize) + self.canvasNew() # vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/MiRCARTToPastebin.py b/MiRCARTToPastebin.py deleted file mode 100644 index 88b25bc..0000000 --- a/MiRCARTToPastebin.py +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env python3 -# -# MiRCARTToPastebin.py -- XXX -# Copyright (c) 2018 Lucio Andrés Illanes Albornoz -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. -# - -from MiRCARTToTextFile import MiRCARTToTextFile -import io -import base64 -import requests, urllib.request - -class MiRCARTToPastebin(): - apiDevKey = outFile = outToTextFile = None - - # export(self): XXX - def export(self): - self.outToTextFile.export(self.outFile, pasteName="", pastePrivate=0) - requestData = { \ - "api_dev_key": self.apiDevKey, \ - "api_option": "paste", \ - "api_paste_code": base64.b64encode(self.outFile.read()), \ - "api_paste_name": pasteName, \ - "api_paste_private": pastePrivate} - responseHttp = requests.post("https://pastebin.com/post.php", \ - data=requestData) - if responseHttp.status_code == 200: - return responseHttp.text - else: - return None - - # __init__(self, canvasMap, canvasSize): XXX - def __init__(self, apiDevKey, canvasMap, canvasSize): - self.apiDevKey = apiDevKey - self.outFile = io.StringIO() - self.outToTextFile = MiRCARTToTextFile(canvasMap, canvasSize) - -# vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/MiRCARTToTextFile.py b/MiRCARTToTextFile.py deleted file mode 100644 index ee0950d..0000000 --- a/MiRCARTToTextFile.py +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env python3 -# -# MiRCARTToTextFile.py -- XXX -# Copyright (c) 2018 Lucio Andrés Illanes Albornoz -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. -# - -class MiRCARTToTextFile(): - canvasMap = canvasSize = None - - # export(self, outFile): XXX - def export(self, outFile): - for canvasRow in range(0, self.canvasSize[1]): - canvasLastColours = [] - for canvasCol in range(0, self.canvasSize[0]): - canvasColColours = self.canvasMap[canvasRow][canvasCol][0:2] - canvasColText = self.canvasMap[canvasRow][canvasCol][2] - if canvasColColours != canvasLastColours: - canvasLastColours = canvasColColours - outFile.write("\x03" + \ - str(canvasColColours[0]) + \ - "," + str(canvasColColours[1])) - outFile.write(canvasColText) - outFile.write("\n") - - # __init__(self, canvasMap, canvasSize): XXX - def __init__(self, canvasMap, canvasSize): - self.canvasMap = canvasMap; self.canvasSize = canvasSize; - -# vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 From 1c5f869c408118afc2cc56931d22e1f44079b32f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Sun, 7 Jan 2018 15:07:37 +0100 Subject: [PATCH 074/148] MiRCARTFrame.py: remove obsolete MiRCARTTo{Pastebin,PngFile} imports. MiRCARTFrame.py:MiRCARTFrame.__init__(): initially disable (grey out) Copy, Cut, Delete, Paste, Export to Pastebin, and Export as PNG menu items. --- MiRCARTFrame.py | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/MiRCARTFrame.py b/MiRCARTFrame.py index e60c41a..38bf7ce 100644 --- a/MiRCARTFrame.py +++ b/MiRCARTFrame.py @@ -26,18 +26,6 @@ from MiRCARTCanvas import MiRCARTCanvas from MiRCARTColours import MiRCARTColours import os, wx -try: - from MiRCARTToPastebin import MiRCARTToPastebin - haveMiRCARTToPastebin = True -except ImportError: - haveMiRCARTToPastebin = False - -try: - from MiRCARTToPngFile import MiRCARTToPngFile - haveMiRCARTToPngFile = True -except ImportError: - haveMiRCARTToPngFile = False - class MiRCARTFrame(wx.Frame): """XXX""" panelSkin = panelCanvas = canvasPathName = None @@ -331,10 +319,12 @@ class MiRCARTFrame(wx.Frame): self._initMenus(self.menuBar, \ [self.MID_FILE, self.MID_EDIT, self.MID_TOOLS], self.onFrameCommand) self.SetMenuBar(self.menuBar) - if not haveMiRCARTToPastebin: - self.menuItemsById[self.CID_EXPORT_PASTEBIN[0]].Enable(False) - if not haveMiRCARTToPngFile: - self.menuItemsById[self.CID_EXPORT_AS_PNG[0]].Enable(False) + self.menuItemsById[self.CID_COPY[0]].Enable(False) + self.menuItemsById[self.CID_CUT[0]].Enable(False) + self.menuItemsById[self.CID_DELETE[0]].Enable(False) + self.menuItemsById[self.CID_PASTE[0]].Enable(False) + self.menuItemsById[self.CID_EXPORT_PASTEBIN[0]].Enable(False) + self.menuItemsById[self.CID_EXPORT_AS_PNG[0]].Enable(False) self.toolBar = wx.ToolBar(self.panelSkin, -1, \ style=wx.HORIZONTAL|wx.TB_FLAT|wx.TB_NODIVIDER) From f6911293c32209300d43fab328fc66d378baeaf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Sun, 7 Jan 2018 15:20:27 +0100 Subject: [PATCH 075/148] MiRCARTFrame:MiRCARTFrame.TID_SELECT, [CM]ID_*: adds selectable item type. MiRCARTFrame:MiRCARTFrame.{_initMenus,__init__}(): automatically set menu item initial state. --- MiRCARTFrame.py | 51 ++++++++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/MiRCARTFrame.py b/MiRCARTFrame.py index 38bf7ce..058dae0 100644 --- a/MiRCARTFrame.py +++ b/MiRCARTFrame.py @@ -36,30 +36,31 @@ class MiRCARTFrame(wx.Frame): TID_COMMAND = (0x001) TID_NOTHING = (0x002) TID_MENU = (0x003) - TID_TOOLBAR = (0x004) - TID_ACCELS = (0x005) + TID_SELECT = (0x004) + TID_TOOLBAR = (0x005) + TID_ACCELS = (0x006) # }}} # {{{ Commands - # Id Type Id Labels Icon bitmap Accelerator + # Id Type Id Labels Icon bitmap Accelerator [Initial state] CID_NEW = (0x100, TID_COMMAND, "New", "&New", [wx.ART_NEW], None) CID_OPEN = (0x101, TID_COMMAND, "Open", "&Open", [wx.ART_FILE_OPEN], None) CID_SAVE = (0x102, TID_COMMAND, "Save", "&Save", [wx.ART_FILE_SAVE], None) CID_SAVEAS = (0x103, TID_COMMAND, "Save As...", "Save &As...", [wx.ART_FILE_SAVE_AS], None) - CID_EXPORT_AS_PNG = (0x104, TID_COMMAND, "Export as PNG...", "Export as PN&G...", (), None) - CID_EXPORT_PASTEBIN = (0x105, TID_COMMAND, "Export to Pastebin...", "Export to Pasteb&in...", (), None) + CID_EXPORT_AS_PNG = (0x104, TID_COMMAND, "Export as PNG...", "Export as PN&G...", (), None, False) + CID_EXPORT_PASTEBIN = (0x105, TID_COMMAND, "Export to Pastebin...", "Export to Pasteb&in...", (), None, False) CID_EXIT = (0x106, TID_COMMAND, "Exit", "E&xit", (), None) - CID_UNDO = (0x107, TID_COMMAND, "Undo", "&Undo", [wx.ART_UNDO], (wx.ACCEL_CTRL, ord("Z"))) - CID_REDO = (0x108, TID_COMMAND, "Redo", "&Redo", [wx.ART_REDO], (wx.ACCEL_CTRL, ord("Y"))) - CID_CUT = (0x109, TID_COMMAND, "Cut", "Cu&t", [wx.ART_CUT], None) - CID_COPY = (0x10a, TID_COMMAND, "Copy", "&Copy", [wx.ART_COPY], None) - CID_PASTE = (0x10b, TID_COMMAND, "Paste", "&Paste", [wx.ART_PASTE], None) - CID_DELETE = (0x10c, TID_COMMAND, "Delete", "De&lete", [wx.ART_DELETE], None) + CID_UNDO = (0x107, TID_COMMAND, "Undo", "&Undo", [wx.ART_UNDO], (wx.ACCEL_CTRL, ord("Z")), False) + CID_REDO = (0x108, TID_COMMAND, "Redo", "&Redo", [wx.ART_REDO], (wx.ACCEL_CTRL, ord("Y")), False) + CID_CUT = (0x109, TID_COMMAND, "Cut", "Cu&t", [wx.ART_CUT], None, False) + CID_COPY = (0x10a, TID_COMMAND, "Copy", "&Copy", [wx.ART_COPY], None, False) + CID_PASTE = (0x10b, TID_COMMAND, "Paste", "&Paste", [wx.ART_PASTE], None, False) + CID_DELETE = (0x10c, TID_COMMAND, "Delete", "De&lete", [wx.ART_DELETE], None, False) CID_INCRBRUSH = (0x10d, TID_COMMAND, "Increase brush size", "&Increase brush size", [wx.ART_PLUS], None) CID_DECRBRUSH = (0x10e, TID_COMMAND, "Decrease brush size", "&Decrease brush size", [wx.ART_MINUS], None) - CID_SOLIDBRUSH = (0x10f, TID_COMMAND, "Solid brush", "&Solid brush", [None], None) - CID_RECT = (0x110, TID_COMMAND, "Rectangle", "&Rectangle", [None], None) - CID_CIRCLE = (0x111, TID_COMMAND, "Circle", "&Circle", [None], None) - CID_LINE = (0x112, TID_COMMAND, "Line", "&Line", [None], None) + CID_SOLID_BRUSH = (0x10f, TID_SELECT, "Solid brush", "&Solid brush", [None], None, True) + CID_RECT = (0x110, TID_SELECT, "Rectangle", "&Rectangle", [None], None, True) + CID_CIRCLE = (0x111, TID_SELECT, "Circle", "&Circle", [None], None, False) + CID_LINE = (0x112, TID_SELECT, "Line", "&Line", [None], None, False) CID_COLOUR00 = (0x113, TID_COMMAND, "Colour #00", "Colour #00", MiRCARTColours[0], None) CID_COLOUR01 = (0x114, TID_COMMAND, "Colour #01", "Colour #01", MiRCARTColours[1], None) CID_COLOUR02 = (0x115, TID_COMMAND, "Colour #02", "Colour #02", MiRCARTColours[2], None) @@ -89,7 +90,7 @@ class MiRCARTFrame(wx.Frame): MID_EDIT = (0x301, TID_MENU, "Edit", "&Edit", ( \ CID_UNDO, CID_REDO, NID_MENU_SEP, \ CID_CUT, CID_COPY, CID_PASTE, CID_DELETE, NID_MENU_SEP, \ - CID_INCRBRUSH, CID_DECRBRUSH, CID_SOLIDBRUSH)) + CID_INCRBRUSH, CID_DECRBRUSH, CID_SOLID_BRUSH)) MID_TOOLS = (0x302, TID_MENU, "Tools", "&Tools", ( \ CID_RECT, CID_CIRCLE, CID_LINE)) # }}} @@ -98,7 +99,7 @@ class MiRCARTFrame(wx.Frame): CID_NEW, CID_OPEN, CID_SAVE, CID_SAVEAS, NID_TOOLBAR_SEP, \ CID_UNDO, CID_REDO, NID_TOOLBAR_SEP, \ CID_CUT, CID_COPY, CID_PASTE, CID_DELETE, NID_TOOLBAR_SEP, \ - CID_INCRBRUSH, CID_DECRBRUSH, CID_SOLIDBRUSH, NID_TOOLBAR_SEP, \ + CID_INCRBRUSH, CID_DECRBRUSH, CID_SOLID_BRUSH, NID_TOOLBAR_SEP, \ CID_RECT, CID_CIRCLE, CID_LINE, NID_TOOLBAR_SEP, \ CID_COLOUR00, CID_COLOUR01, CID_COLOUR02, CID_COLOUR03, CID_COLOUR04, \ CID_COLOUR05, CID_COLOUR06, CID_COLOUR07, CID_COLOUR08, CID_COLOUR09, \ @@ -136,10 +137,18 @@ class MiRCARTFrame(wx.Frame): for menuItem in menuDescr[4]: if menuItem == self.NID_MENU_SEP: menuWindow.AppendSeparator() + elif menuItem[1] == self.TID_SELECT: + menuItemWindow = menuWindow.AppendRadioItem(menuItem[0], menuItem[3], menuItem[2]) + self.menuItemsById[menuItem[0]] = menuItemWindow + self.Bind(wx.EVT_MENU, handler, menuItemWindow) + if len(menuItem) == 7: + menuItemWindow.Check(menuItem[6]) else: menuItemWindow = menuWindow.Append(menuItem[0], menuItem[3], menuItem[2]) self.menuItemsById[menuItem[0]] = menuItemWindow self.Bind(wx.EVT_MENU, handler, menuItemWindow) + if len(menuItem) == 7: + menuItemWindow.Enable(menuItem[6]) menuBar.Append(menuWindow, menuDescr[3]) # }}} # {{{ _initToolBars(self, toolBar, toolBarsDescr, handler): XXX @@ -284,7 +293,7 @@ class MiRCARTFrame(wx.Frame): pass elif cid == self.CID_DECRBRUSH[0]: pass - elif cid == self.CID_SOLIDBRUSH[0]: + elif cid == self.CID_SOLID_BRUSH[0]: pass elif cid == self.CID_RECT[0]: pass @@ -319,12 +328,6 @@ class MiRCARTFrame(wx.Frame): self._initMenus(self.menuBar, \ [self.MID_FILE, self.MID_EDIT, self.MID_TOOLS], self.onFrameCommand) self.SetMenuBar(self.menuBar) - self.menuItemsById[self.CID_COPY[0]].Enable(False) - self.menuItemsById[self.CID_CUT[0]].Enable(False) - self.menuItemsById[self.CID_DELETE[0]].Enable(False) - self.menuItemsById[self.CID_PASTE[0]].Enable(False) - self.menuItemsById[self.CID_EXPORT_PASTEBIN[0]].Enable(False) - self.menuItemsById[self.CID_EXPORT_AS_PNG[0]].Enable(False) self.toolBar = wx.ToolBar(self.panelSkin, -1, \ style=wx.HORIZONTAL|wx.TB_FLAT|wx.TB_NODIVIDER) From 84ab4eee5a4c77bc970e58d20b072e60eee82161 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Sun, 7 Jan 2018 16:43:41 +0100 Subject: [PATCH 076/148] Initial release sans tools. --- IrcClient.py | 2 +- IrcMiRCARTBot.py | 5 +- MiRCARTCanvas.py | 108 ++++++++++---- MiRCARTCanvasJournal.py | 51 ++++--- MiRCARTCanvasStore.py | 106 ++++++------- MiRCARTColours.py | 34 ++--- MiRCARTFrame.py | 322 ++++++++++++++++++++-------------------- MiRCARTGeneralFrame.py | 140 +++++++++++++++++ MiRCARTToPngFile.py | 31 ++-- MiRCARTTool.py | 4 +- MiRCARTToolRect.py | 28 ++-- README.md | 4 +- 12 files changed, 503 insertions(+), 332 deletions(-) create mode 100644 MiRCARTGeneralFrame.py diff --git a/IrcClient.py b/IrcClient.py index 0816558..1d8647a 100644 --- a/IrcClient.py +++ b/IrcClient.py @@ -90,7 +90,7 @@ class IrcClient: # {{{ queue(self, *args): Parse and queue single line to server from list def queue(self, *args): msg = ""; argNumMax = len(args); - for argNum in range(0, argNumMax): + for argNum in range(argNumMax): if argNum == (argNumMax - 1): msg += ":" + args[argNum] else: diff --git a/IrcMiRCARTBot.py b/IrcMiRCARTBot.py index 6d75554..b943f23 100755 --- a/IrcMiRCARTBot.py +++ b/IrcMiRCARTBot.py @@ -27,6 +27,7 @@ import os, sys, time import json import IrcClient import requests, urllib.request +from MiRCARTCanvasStore import MiRCARTCanvasStore from MiRCARTToPngFile import MiRCARTToPngFile class IrcMiRCARTBot(IrcClient.IrcClient): @@ -139,7 +140,9 @@ class IrcMiRCARTBot(IrcClient.IrcClient): self._log("Unknown URL type specified!") self.queue("PRIVMSG", message[2], "4/!\\ Unknown URL type specified!") return - MiRCARTToPngFile(asciiTmpFilePath, "DejaVuSansMono.ttf", 11).export(imgTmpFilePath) + + canvasStore = MiRCARTCanvasStore(inFile=asciiTmpFilePath) + MiRCARTToPngFile(canvasStore.outMap, "DejaVuSansMono.ttf", 11).export(imgTmpFilePath) imgurResponse = self._uploadToImgur(imgTmpFilePath, "MiRCART image", "MiRCART image", "c9a6efb3d7932fd") if imgurResponse[0] == 200: self._log("Uploaded as: {}".format(imgurResponse[1])) diff --git a/MiRCARTCanvas.py b/MiRCARTCanvas.py index f8b5a47..0503889 100644 --- a/MiRCARTCanvas.py +++ b/MiRCARTCanvas.py @@ -23,40 +23,43 @@ # from MiRCARTCanvasJournal import MiRCARTCanvasJournal -from MiRCARTCanvasStore import MiRCARTCanvasStore +from MiRCARTCanvasStore import MiRCARTCanvasStore, haveMiRCARTToPngFile, haveUrllib from MiRCARTColours import MiRCARTColours import wx class MiRCARTCanvas(wx.Panel): """XXX""" parentFrame = None - canvasPos = canvasSize = canvasWinSize = cellPos = cellSize = None + canvasPos = canvasSize = canvasWinSize = cellSize = None canvasBitmap = canvasMap = canvasTools = None - mircBg = mircFg = mircBrushes = mircPens = None + brushColours = brushPos = brushSize = None + mircBrushes = mircPens = None canvasJournal = canvasStore = None # {{{ _initBrushesAndPens(self): XXX def _initBrushesAndPens(self): self.mircBrushes = [None for x in range(len(MiRCARTColours))] self.mircPens = [None for x in range(len(MiRCARTColours))] - for mircColour in range(0, len(MiRCARTColours)): + for mircColour in range(len(MiRCARTColours)): self.mircBrushes[mircColour] = wx.Brush( \ - wx.Colour(MiRCARTColours[mircColour]), wx.BRUSHSTYLE_SOLID) + wx.Colour(MiRCARTColours[mircColour][0:4]), wx.BRUSHSTYLE_SOLID) self.mircPens[mircColour] = wx.Pen( \ - wx.Colour(MiRCARTColours[mircColour]), 1) + wx.Colour(MiRCARTColours[mircColour][0:4]), 1) # }}} # {{{ _drawPatch(self, patch, eventDc, tmpDc, atPoint): XXX def _drawPatch(self, patch, eventDc, tmpDc, atPoint): - absPoint = self._relMapPointToAbsPoint((patch[0], patch[1]), atPoint) - if patch[4] == " ": - brushFg = self.mircBrushes[patch[3]]; brushBg = self.mircBrushes[patch[3]]; - pen = self.mircPens[patch[3]] + absPoint = self._relMapPointToAbsPoint(patch[0], atPoint) + if patch[3] == " ": + brushFg = self.mircBrushes[patch[1][1]] + brushBg = self.mircBrushes[patch[1][0]] + pen = self.mircPens[patch[1][1]] else: - brushFg = self.mircBrushes[patch[2]]; brushBg = self.mircBrushes[patch[3]]; - pen = self.mircPens[patch[2]] + brushFg = self.mircBrushes[patch[1][0]] + brushBg = self.mircBrushes[patch[1][1]] + pen = self.mircPens[patch[1][0]] for dc in (eventDc, tmpDc): dc.SetBrush(brushFg); dc.SetBackground(brushBg); dc.SetPen(pen); - dc.DrawRectangle(absPoint[0], absPoint[1], self.cellSize[0], self.cellSize[1]) + dc.DrawRectangle(*absPoint, *self.cellSize) # }}} # {{{ _eventPointToMapPoint(self, eventPoint): XXX def _eventPointToMapPoint(self, eventPoint): @@ -72,13 +75,11 @@ class MiRCARTCanvas(wx.Panel): # }}} # {{{ _relMapPointToAbsPoint(self, relMapPoint, atPoint): XXX def _relMapPointToAbsPoint(self, relMapPoint, atPoint): - absX = (atPoint[0] + relMapPoint[0]) * self.cellSize[0] - absY = (atPoint[1] + relMapPoint[1]) * self.cellSize[1] - return (absX, absY) + return [(a+b)*c for a,b,c in zip(atPoint, relMapPoint, self.cellSize)] # }}} - # {{{ _setMapCell(self, absMapPoint, colourFg, colourBg, char): XXX - def _setMapCell(self, absMapPoint, colourFg, colourBg, char): - self.canvasMap[absMapPoint[1]][absMapPoint[0]] = [colourFg, colourBg, char] + # {{{ _setMapCell(self, absMapPoint, colours, charAttrs, char): XXX + def _setMapCell(self, absMapPoint, colours, charAttrs, char): + self.canvasMap[absMapPoint[1]][absMapPoint[0]] = [colours, charAttrs, char] # }}} # {{{ onClose(self, event): XXX @@ -86,17 +87,24 @@ class MiRCARTCanvas(wx.Panel): self.Destroy(); self.__del__(); # }}} # {{{ onJournalUpdate(self, isTmp, absMapPoint, patch, eventDc, tmpDc, atPoint): - def onJournalUpdate(self, isTmp, absMapPoint, patch, eventDc, tmpDc, atPoint): + def onJournalUpdate(self, isTmp, absMapPoint, patch, eventDc, tmpDc, atPoint, isInherit=False): if eventDc == None: eventDc = wx.ClientDC(self); tmpDc = wx.MemoryDC(); if tmpDc == None: tmpDc.SelectObject(self.canvasBitmap) if isTmp == True: + if isInherit: + patch[1:] = self._getMapCell(patch[0]) self._drawPatch(patch, eventDc, tmpDc, atPoint) else: - self._setMapCell(absMapPoint, *patch[2:5]) + if isInherit: + patchOld = patch.copy() + patchOld[1:] = self._getMapCell(patchOld[0]) + self._setMapCell(absMapPoint, *patch[1:]) self._drawPatch(patch, eventDc, tmpDc, atPoint) self.parentFrame.onCanvasUpdate() + if isInherit: + return patchOld # }}} # {{{ onMouseEvent(self, event): XXX def onMouseEvent(self, event): @@ -106,14 +114,58 @@ class MiRCARTCanvas(wx.Panel): eventPoint = event.GetLogicalPosition(eventDc) mapPoint = self._eventPointToMapPoint(eventPoint) for tool in self.canvasTools: - mapPatches = tool.onMouseEvent(event, mapPoint, event.Dragging(), \ - event.LeftIsDown(), event.RightIsDown()) + mapPatches = tool.onMouseEvent( \ + event, mapPoint, self.brushColours, self.brushSize, \ + event.Dragging(), event.LeftIsDown(), event.RightIsDown()) self.canvasJournal.merge(mapPatches, eventDc, tmpDc, mapPoint) + self.parentFrame.onCanvasMotion(event, mapPoint) + # }}} + # {{{ onMouseWindowEvent(self, event): XXX + def onMouseWindowEvent(self, event): + eventObject = event.GetEventObject() + eventDc = wx.ClientDC(self); tmpDc = wx.MemoryDC(); + tmpDc.SelectObject(self.canvasBitmap) + self.canvasJournal.resetCursor(eventDc, tmpDc) + self.parentFrame.onCanvasMotion(event) # }}} # {{{ onPaint(self, event): XXX def onPaint(self, event): eventDc = wx.BufferedPaintDC(self, self.canvasBitmap) # }}} + # {{{ onStoreUpdate(self, newCanvasSize, newCanvas=None): + def onStoreUpdate(self, newCanvasSize, newCanvas=None): + if newCanvasSize != None: + self.resize(newCanvasSize) + self.canvasJournal.reset() + if newCanvas != None: + self.canvasMap = newCanvas.copy() + for numRow in range(self.canvasSize[1]): + numRowCols = len(self.canvasMap[numRow]) + if numRowCols < self.canvasSize[0]: + colsDelta = self.canvasSize[0] - numRowCols + self.canvasMap[numRow][self.canvasSize[0]:] = \ + [[(1, 1), 0, " "] for y in range(colsDelta)] + else: + del self.canvasMap[numRow][self.canvasSize[0]:] + else: + self.canvasMap = [[[(1, 1), 0, " "] \ + for x in range(self.canvasSize[0])] \ + for y in range(self.canvasSize[1])] + canvasWinSize = [a*b for a,b in zip(self.canvasSize, self.cellSize)] + if self.canvasBitmap != None: + self.canvasBitmap.Destroy() + self.canvasBitmap = wx.Bitmap(canvasWinSize) + eventDc = wx.ClientDC(self); tmpDc = wx.MemoryDC(); + tmpDc.SelectObject(self.canvasBitmap) + for numRow in range(self.canvasSize[1]): + for numCol in range(self.canvasSize[0]): + self.onJournalUpdate(False, \ + (numCol, numRow), \ + [(numCol, numRow), \ + *self.canvasMap[numRow][numCol]], \ + eventDc, tmpDc, (0, 0)) + wx.SafeYield() + # }}} # {{{ redo(self): XXX def redo(self): return self.canvasJournal.redo() @@ -124,12 +176,12 @@ class MiRCARTCanvas(wx.Panel): self.SetSize(*self.canvasPos, \ newCanvasSize[0] * self.cellSize[0], \ newCanvasSize[1] * self.cellSize[1]) - for numRow in range(0, self.canvasSize[1]): + for numRow in range(self.canvasSize[1]): for numNewCol in range(self.canvasSize[0], newCanvasSize[0]): self.canvasMap[numRow].append([1, 1, " "]) for numNewRow in range(self.canvasSize[1], newCanvasSize[1]): self.canvasMap.append([]) - for numNewCol in range(0, newCanvasSize[0]): + for numNewCol in range(newCanvasSize[0]): self.canvasMap[numNewRow].append([1, 1, " "]) self.canvasSize = newCanvasSize canvasWinSize = ( \ @@ -162,14 +214,16 @@ class MiRCARTCanvas(wx.Panel): self.canvasTools = [canvasTool(self) for canvasTool in canvasTools] self.cellSize = cellSize - self.cellPos = (0, 0) - self.mircBg = 1; self.mircFg = 4; self._initBrushesAndPens(); + self.brushColours = [4, 1]; self._initBrushesAndPens(); + self.brushPos = [0, 0]; self.brushSize = [1, 1]; self.canvasJournal = MiRCARTCanvasJournal(parentCanvas=self) self.canvasStore = MiRCARTCanvasStore(parentCanvas=self) super().__init__(parent, pos=canvasPos, \ size=[w*h for w,h in zip(canvasSize, cellSize)]) self.Bind(wx.EVT_CLOSE, self.onClose) + self.Bind(wx.EVT_ENTER_WINDOW, self.onMouseWindowEvent) + self.Bind(wx.EVT_LEAVE_WINDOW, self.onMouseWindowEvent) self.Bind(wx.EVT_LEFT_DOWN, self.onMouseEvent) self.Bind(wx.EVT_MOTION, self.onMouseEvent) self.Bind(wx.EVT_PAINT, self.onPaint) diff --git a/MiRCARTCanvasJournal.py b/MiRCARTCanvasJournal.py index a712d36..3b70dbc 100644 --- a/MiRCARTCanvasJournal.py +++ b/MiRCARTCanvasJournal.py @@ -31,28 +31,26 @@ class MiRCARTCanvasJournal(): def _popTmp(self, eventDc, tmpDc): if self.patchesTmp: for patch in self.patchesTmp: - patch[2:] = self.parentCanvas._getMapCell([patch[0], patch[1]]) - self.parentCanvas.onJournalUpdate(True, \ - (patch[0:2]), patch, eventDc, tmpDc, (0, 0)) + self.parentCanvas.onJournalUpdate(True, \ + patch[0], patch, eventDc, tmpDc, (0, 0), True) self.patchesTmp = [] # }}} # {{{ _pushTmp(self, atPoint, patch): XXX def _pushTmp(self, absMapPoint): - self.patchesTmp.append([*absMapPoint, None, None, None]) + self.patchesTmp.append([absMapPoint, None, None, None]) # }}} - # {{{ _pushUndo(self, atPoint, patch): XXX - def _pushUndo(self, atPoint, patch, mapItem): + # {{{ _pushUndo(self, atPoint, patchUndo, patchRedo): XXX + def _pushUndo(self, atPoint, patchUndo, patchRedo): if self.patchesUndoLevel > 0: del self.patchesUndo[0:self.patchesUndoLevel] self.patchesUndoLevel = 0 - absMapPoint = self._relMapPointToAbsMapPoint((patch[0], patch[1]), atPoint) - self.patchesUndo.insert(0, ( \ - (absMapPoint[0], absMapPoint[1], mapItem[0], mapItem[1], mapItem[2]), \ - (absMapPoint[0], absMapPoint[1], patch[2], patch[3], patch[4]))) + absMapPoint = self._relMapPointToAbsMapPoint(patchUndo[0], atPoint) + self.patchesUndo.insert(0, [ \ + [absMapPoint, *patchUndo[1:]], [absMapPoint, *patchRedo[1:]]]) # }}} # {{{ _relMapPointToAbsMapPoint(self, relMapPoint, atPoint): XXX def _relMapPointToAbsMapPoint(self, relMapPoint, atPoint): - return (atPoint[0] + relMapPoint[0], atPoint[1] + relMapPoint[1]) + return [a+b for a,b in zip(atPoint, relMapPoint)] # }}} # {{{ merge(self, mapPatches, eventDc, tmpDc, atPoint): XXX def merge(self, mapPatches, eventDc, tmpDc, atPoint): @@ -61,40 +59,45 @@ class MiRCARTCanvasJournal(): if mapPatchTmp: self._popTmp(eventDc, tmpDc) for patch in mapPatch[1]: - absMapPoint = self._relMapPointToAbsMapPoint(patch[0:2], atPoint) - mapItem = self.parentCanvas._getMapCell(absMapPoint) + absMapPoint = self._relMapPointToAbsMapPoint(patch[0], atPoint) if mapPatchTmp: self._pushTmp(absMapPoint) - self.parentCanvas.onJournalUpdate(mapPatchTmp, \ - absMapPoint, patch, eventDc, tmpDc, atPoint) - elif mapItem != patch[2:5]: - self._pushUndo(atPoint, patch, mapItem) - self.parentCanvas.onJournalUpdate(mapPatchTmp, \ + self.parentCanvas.onJournalUpdate(mapPatchTmp, \ absMapPoint, patch, eventDc, tmpDc, atPoint) + else: + patchUndo = \ + self.parentCanvas.onJournalUpdate(mapPatchTmp, \ + absMapPoint, patch, eventDc, tmpDc, atPoint, True) + self._pushUndo(atPoint, patchUndo, patch) # }}} # {{{ redo(self): XXX def redo(self): if self.patchesUndoLevel > 0: self.patchesUndoLevel -= 1 redoPatch = self.patchesUndo[self.patchesUndoLevel][1] - self.parentCanvas.onJournalUpdate(False, \ - (redoPatch[0:2]), redoPatch, None, None, (0, 0)) + self.parentCanvas.onJournalUpdate(False, \ + redoPatch[0], redoPatch, None, None, (0, 0)) return True else: return False # }}} # {{{ reset(self): XXX def reset(self): - self.patchesTmp = [] - self.patchesUndo = [None]; self.patchesUndoLevel = 0; + self.patchesTmp = []; self.patchesUndo = [None]; self.patchesUndoLevel = 0; + # }}} + # {{{ resetCursor(self, eventDc, tmpDc): XXX + def resetCursor(self, eventDc, tmpDc): + if len(self.patchesTmp): + self._popTmp(eventDc, tmpDc) + self.patchesTmp = [] # }}} # {{{ undo(self): XXX def undo(self): if self.patchesUndo[self.patchesUndoLevel] != None: undoPatch = self.patchesUndo[self.patchesUndoLevel][0] self.patchesUndoLevel += 1 - self.parentCanvas.onJournalUpdate(False, \ - (undoPatch[0:2]), undoPatch, None, None, (0, 0)) + self.parentCanvas.onJournalUpdate(False, \ + undoPatch[0], undoPatch, None, None, (0, 0)) return True else: return False diff --git a/MiRCARTCanvasStore.py b/MiRCARTCanvasStore.py index a3b106e..8a65cd2 100644 --- a/MiRCARTCanvasStore.py +++ b/MiRCARTCanvasStore.py @@ -22,9 +22,13 @@ # SOFTWARE. # -import base64 import io -import wx + +try: + import wx + haveWx = True +except ImportError: + haveWx = False try: from MiRCARTToPngFile import MiRCARTToPngFile @@ -78,66 +82,59 @@ class MiRCARTCanvasStore(): return cellState | bit # }}} - # {{{ exportPastebin(self, apiDevKey): XXX - def exportPastebin(self, apiDevKey): + # {{{ exportBitmapToPngFile(self, canvasBitmap, outPathName, outType): XXX + def exportBitmapToPngFile(self, canvasBitmap, outPathName, outType): + return canvasBitmap.ConvertToImage().SaveFile(outPathName, outType) + # }}} + # {{{ exportPastebin(self, apiDevKey, canvasMap, canvasSize, pasteName="", pastePrivate=0): XXX + def exportPastebin(self, apiDevKey, canvasMap, canvasSize, pasteName="", pastePrivate=0): if haveUrllib: - outFile = io.StringIO(); self.exportTextFile(outFile); - requestData = { \ - "api_dev_key": self.apiDevKey, \ - "api_option": "paste", \ - "api_paste_code": base64.b64encode(outFile.read()), \ - "api_paste_name": pasteName, \ + outFile = io.StringIO() + self.exportTextFile(canvasMap, canvasSize, outFile) + requestData = { \ + "api_dev_key": apiDevKey, \ + "api_option": "paste", \ + "api_paste_code": outFile.getvalue().encode(), \ + "api_paste_name": pasteName, \ "api_paste_private": pastePrivate} - responseHttp = requests.post("https://pastebin.com/post.php", \ + responseHttp = requests.post("https://pastebin.com/api/api_post.php", \ data=requestData) if responseHttp.status_code == 200: - return responseHttp.text + if responseHttp.text.startswith("http"): + return (True, responseHttp.text) + else: + return (False, responseHttp.text) else: - return None + return (False, str(responseHttp.status_code)) else: - return None + return (False, "missing requests and/or urllib3 module(s)") # }}} - # {{{ exportPngFile(self): XXX - def exportPngFile(self, pathName): + # {{{ exportPngFile(self, canvasMap, outPathName): XXX + def exportPngFile(self, canvasMap, outPathName): if haveMiRCARTToPngFile: - outFile = io.StringIO(); self.exportTextFile(outFile); - MiRCARTToPng(outFile).export(pathName) + MiRCARTToPngFile(canvasMap).export(outPathName) return True else: return False # }}} - # {{{ exportTextFile(self, outFile): XXX - def exportTextFile(self, outFile): - canvasMap = self.parentCanvas.canvasMap - canvasSize = self.parentCanvas.canvasSize - for canvasRow in range(0, canvasSize[1]): + # {{{ exportTextFile(self, canvasMap, canvasSize, outFile): XXX + def exportTextFile(self, canvasMap, canvasSize, outFile): + for canvasRow in range(canvasSize[1]): canvasLastColours = [] - for canvasCol in range(0, canvasSize[0]): - canvasColColours = canvasMap[canvasRow][canvasCol][0:2] - canvasColText = self.canvasMap[canvasRow][canvasCol][2] - if canvasColColours != canvasLastColours: - canvasLastColours = canvasColColours - outFile.write("\x03" + \ - str(canvasColColours[0]) + \ - "," + str(canvasColColours[1])) + for canvasCol in range(canvasSize[0]): + canvasColColours = canvasMap[canvasRow][canvasCol][0] + canvasColText = canvasMap[canvasRow][canvasCol][2] + if canvasColColours != canvasLastColours: + canvasLastColours = canvasColColours + outFile.write("\x03" + \ + str(canvasColColours[0]) + \ + "," + str(canvasColColours[1])) outFile.write(canvasColText) outFile.write("\n") # }}} # {{{ importIntoPanel(self): XXX def importIntoPanel(self): - canvasSize = self.inSize; self.parentCanvas.resize(canvasSize); - self.parentCanvas.canvasJournal.reset() - eventDc = wx.ClientDC(self.parentCanvas); tmpDc = wx.MemoryDC(); - tmpDc.SelectObject(self.parentCanvas.canvasBitmap) - for numRow in range(0, len(self.outMap)): - for numCol in range(0, len(self.outMap[numRow])): - self.parentCanvas.onJournalUpdate(False, \ - (numCol, numRow), [numCol, numRow, \ - self.outMap[numRow][numCol][0][0], \ - self.outMap[numRow][numCol][0][1], \ - self.outMap[numRow][numCol][2]], \ - eventDc, tmpDc, (0, 0)) - wx.SafeYield() + self.parentCanvas.onStoreUpdate(self.inSize, self.outMap) # }}} # {{{ importTextFile(self, pathName): XXX def importTextFile(self, pathName): @@ -210,26 +207,7 @@ class MiRCARTCanvasStore(): # }}} # {{{ importNew(self, newCanvasSize=None): XXX def importNew(self, newCanvasSize=None): - if newCanvasSize != None: - self.parentCanvas.resize(newCanvasSize) - self.parentCanvas.canvasJournal.reset() - self.parentCanvas.canvasMap = [[[1, 1, " "] \ - for x in range(self.parentCanvas.canvasSize[0])] \ - for y in range(self.parentCanvas.canvasSize[1])] - canvasWinSize = ( \ - self.parentCanvas.cellSize[0] * self.parentCanvas.canvasSize[0], \ - self.parentCanvas.cellSize[1] * self.parentCanvas.canvasSize[1]) - if self.parentCanvas.canvasBitmap != None: - self.parentCanvas.canvasBitmap.Destroy() - self.parentCanvas.canvasBitmap = wx.Bitmap(canvasWinSize) - eventDc = wx.ClientDC(self.parentCanvas); tmpDc = wx.MemoryDC(); - tmpDc.SelectObject(self.parentCanvas.canvasBitmap) - for numRow in range(0, len(self.parentCanvas.canvasMap)): - for numCol in range(0, len(self.parentCanvas.canvasMap[numRow])): - self.parentCanvas.onJournalUpdate(False, \ - (numCol, numRow), [numCol, numRow, 1, 1, " "], \ - eventDc, tmpDc, (0, 0)) - wx.SafeYield() + self.parentCanvas.onStoreUpdate(newCanvasSize) # }}} # diff --git a/MiRCARTColours.py b/MiRCARTColours.py index 9489b08..2475cc6 100755 --- a/MiRCARTColours.py +++ b/MiRCARTColours.py @@ -23,25 +23,25 @@ # # -# MiRCARTColours: mIRC colour number to RGBA map given none of ^[BFV_] (bold, italic, reverse, underline) +# MiRCARTColours: mIRC colour number to RGBA map given none of ^[BFV_] (bold, italic, reverse, underline], # MiRCARTColours = [ - (255, 255, 255, 255), # White - (0, 0, 0, 255), # Black - (0, 0, 187, 255), # Blue - (0, 187, 0, 255), # Green - (255, 85, 85, 255), # Light Red - (187, 0, 0, 255), # Red - (187, 0, 187, 255), # Purple - (187, 187, 0, 255), # Yellow - (255, 255, 85, 255), # Light Yellow - (85, 255, 85, 255), # Light Green - (0, 187, 187, 255), # Cyan - (85, 255, 255, 255), # Light Cyan - (85, 85, 255, 255), # Light Blue - (255, 85, 255, 255), # Pink - (85, 85, 85, 255), # Grey - (187, 187, 187, 255), # Light Grey + [255, 255, 255, 255, "White"], + [0, 0, 0, 255, "Black"], + [0, 0, 187, 255, "Blue"], + [0, 187, 0, 255, "Green"], + [255, 85, 85, 255, "Light Red"], + [187, 0, 0, 255, "Red"], + [187, 0, 187, 255, "Purple"], + [187, 187, 0, 255, "Yellow"], + [255, 255, 85, 255, "Light Yellow"], + [85, 255, 85, 255, "Light Green"], + [0, 187, 187, 255, "Cyan"], + [85, 255, 255, 255, "Light Cyan"], + [85, 85, 255, 255, "Light Blue"], + [255, 85, 255, 255, "Pink"], + [85, 85, 85, 255, "Grey"], + [187, 187, 187, 255, "Light Grey"], ] # vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/MiRCARTFrame.py b/MiRCARTFrame.py index 058dae0..79af864 100644 --- a/MiRCARTFrame.py +++ b/MiRCARTFrame.py @@ -22,61 +22,59 @@ # SOFTWARE. # -from MiRCARTCanvas import MiRCARTCanvas +from MiRCARTCanvas import MiRCARTCanvas, haveUrllib from MiRCARTColours import MiRCARTColours +from MiRCARTGeneralFrame import MiRCARTGeneralFrame, \ + TID_COMMAND, TID_MENU, TID_NOTHING, TID_SELECT, TID_TOOLBAR, TID_ACCELS + import os, wx -class MiRCARTFrame(wx.Frame): +class MiRCARTFrame(MiRCARTGeneralFrame): """XXX""" - panelSkin = panelCanvas = canvasPathName = None + panelCanvas = canvasPathName = None canvasPos = canvasSize = canvasTools = cellSize = None - menuItemsById = menuBar = toolBar = accelTable = statusBar = None - # {{{ Types - TID_COMMAND = (0x001) - TID_NOTHING = (0x002) - TID_MENU = (0x003) - TID_SELECT = (0x004) - TID_TOOLBAR = (0x005) - TID_ACCELS = (0x006) - # }}} # {{{ Commands # Id Type Id Labels Icon bitmap Accelerator [Initial state] - CID_NEW = (0x100, TID_COMMAND, "New", "&New", [wx.ART_NEW], None) - CID_OPEN = (0x101, TID_COMMAND, "Open", "&Open", [wx.ART_FILE_OPEN], None) - CID_SAVE = (0x102, TID_COMMAND, "Save", "&Save", [wx.ART_FILE_SAVE], None) - CID_SAVEAS = (0x103, TID_COMMAND, "Save As...", "Save &As...", [wx.ART_FILE_SAVE_AS], None) - CID_EXPORT_AS_PNG = (0x104, TID_COMMAND, "Export as PNG...", "Export as PN&G...", (), None, False) - CID_EXPORT_PASTEBIN = (0x105, TID_COMMAND, "Export to Pastebin...", "Export to Pasteb&in...", (), None, False) - CID_EXIT = (0x106, TID_COMMAND, "Exit", "E&xit", (), None) - CID_UNDO = (0x107, TID_COMMAND, "Undo", "&Undo", [wx.ART_UNDO], (wx.ACCEL_CTRL, ord("Z")), False) - CID_REDO = (0x108, TID_COMMAND, "Redo", "&Redo", [wx.ART_REDO], (wx.ACCEL_CTRL, ord("Y")), False) - CID_CUT = (0x109, TID_COMMAND, "Cut", "Cu&t", [wx.ART_CUT], None, False) - CID_COPY = (0x10a, TID_COMMAND, "Copy", "&Copy", [wx.ART_COPY], None, False) - CID_PASTE = (0x10b, TID_COMMAND, "Paste", "&Paste", [wx.ART_PASTE], None, False) - CID_DELETE = (0x10c, TID_COMMAND, "Delete", "De&lete", [wx.ART_DELETE], None, False) - CID_INCRBRUSH = (0x10d, TID_COMMAND, "Increase brush size", "&Increase brush size", [wx.ART_PLUS], None) - CID_DECRBRUSH = (0x10e, TID_COMMAND, "Decrease brush size", "&Decrease brush size", [wx.ART_MINUS], None) - CID_SOLID_BRUSH = (0x10f, TID_SELECT, "Solid brush", "&Solid brush", [None], None, True) - CID_RECT = (0x110, TID_SELECT, "Rectangle", "&Rectangle", [None], None, True) - CID_CIRCLE = (0x111, TID_SELECT, "Circle", "&Circle", [None], None, False) - CID_LINE = (0x112, TID_SELECT, "Line", "&Line", [None], None, False) - CID_COLOUR00 = (0x113, TID_COMMAND, "Colour #00", "Colour #00", MiRCARTColours[0], None) - CID_COLOUR01 = (0x114, TID_COMMAND, "Colour #01", "Colour #01", MiRCARTColours[1], None) - CID_COLOUR02 = (0x115, TID_COMMAND, "Colour #02", "Colour #02", MiRCARTColours[2], None) - CID_COLOUR03 = (0x116, TID_COMMAND, "Colour #03", "Colour #03", MiRCARTColours[3], None) - CID_COLOUR04 = (0x117, TID_COMMAND, "Colour #04", "Colour #04", MiRCARTColours[4], None) - CID_COLOUR05 = (0x118, TID_COMMAND, "Colour #05", "Colour #05", MiRCARTColours[5], None) - CID_COLOUR06 = (0x119, TID_COMMAND, "Colour #06", "Colour #06", MiRCARTColours[6], None) - CID_COLOUR07 = (0x11a, TID_COMMAND, "Colour #07", "Colour #07", MiRCARTColours[7], None) - CID_COLOUR08 = (0x11b, TID_COMMAND, "Colour #08", "Colour #08", MiRCARTColours[8], None) - CID_COLOUR09 = (0x11c, TID_COMMAND, "Colour #09", "Colour #09", MiRCARTColours[9], None) - CID_COLOUR10 = (0x11d, TID_COMMAND, "Colour #10", "Colour #10", MiRCARTColours[10], None) - CID_COLOUR11 = (0x11e, TID_COMMAND, "Colour #11", "Colour #11", MiRCARTColours[11], None) - CID_COLOUR12 = (0x11f, TID_COMMAND, "Colour #12", "Colour #12", MiRCARTColours[12], None) - CID_COLOUR13 = (0x120, TID_COMMAND, "Colour #13", "Colour #13", MiRCARTColours[13], None) - CID_COLOUR14 = (0x121, TID_COMMAND, "Colour #14", "Colour #14", MiRCARTColours[14], None) - CID_COLOUR15 = (0x122, TID_COMMAND, "Colour #15", "Colour #15", MiRCARTColours[15], None) + CID_NEW = (0x100, TID_COMMAND, "New", "&New", wx.ART_NEW, (wx.ACCEL_CTRL, ord("N"))) + CID_OPEN = (0x101, TID_COMMAND, "Open", "&Open", wx.ART_FILE_OPEN, (wx.ACCEL_CTRL, ord("O"))) + CID_SAVE = (0x102, TID_COMMAND, "Save", "&Save", wx.ART_FILE_SAVE, (wx.ACCEL_CTRL, ord("S"))) + CID_SAVEAS = (0x103, TID_COMMAND, "Save As...", "Save &As...", wx.ART_FILE_SAVE_AS, None) + CID_EXPORT_AS_PNG = (0x104, TID_COMMAND, "Export as PNG...", \ + "Export as PN&G...", None, None) + CID_EXPORT_PASTEBIN = (0x105, TID_COMMAND, "Export to Pastebin...", \ + "Export to Pasteb&in...", None, None, haveUrllib) + CID_EXIT = (0x106, TID_COMMAND, "Exit", "E&xit", None, None) + CID_UNDO = (0x107, TID_COMMAND, "Undo", "&Undo", wx.ART_UNDO, (wx.ACCEL_CTRL, ord("Z")), False) + CID_REDO = (0x108, TID_COMMAND, "Redo", "&Redo", wx.ART_REDO, (wx.ACCEL_CTRL, ord("Y")), False) + CID_CUT = (0x109, TID_COMMAND, "Cut", "Cu&t", wx.ART_CUT, None, False) + CID_COPY = (0x10a, TID_COMMAND, "Copy", "&Copy", wx.ART_COPY, None, False) + CID_PASTE = (0x10b, TID_COMMAND, "Paste", "&Paste", wx.ART_PASTE, None, False) + CID_DELETE = (0x10c, TID_COMMAND, "Delete", "De&lete", wx.ART_DELETE, None, False) + CID_INCRBRUSH = (0x10d, TID_COMMAND, "Increase brush size", \ + "&Increase brush size", wx.ART_PLUS, None) + CID_DECRBRUSH = (0x10e, TID_COMMAND, "Decrease brush size", \ + "&Decrease brush size", wx.ART_MINUS, None) + CID_SOLID_BRUSH = (0x10f, TID_SELECT, "Solid brush", "&Solid brush", None, None, True) + CID_RECT = (0x110, TID_SELECT, "Rectangle", "&Rectangle", None, None, True) + CID_CIRCLE = (0x111, TID_SELECT, "Circle", "&Circle", None, None, False) + CID_LINE = (0x112, TID_SELECT, "Line", "&Line", None, None, False) + CID_COLOUR00 = (0x113, TID_COMMAND, "Colour #00", "Colour #00", None, None) + CID_COLOUR01 = (0x114, TID_COMMAND, "Colour #01", "Colour #01", None, None) + CID_COLOUR02 = (0x115, TID_COMMAND, "Colour #02", "Colour #02", None, None) + CID_COLOUR03 = (0x116, TID_COMMAND, "Colour #03", "Colour #03", None, None) + CID_COLOUR04 = (0x117, TID_COMMAND, "Colour #04", "Colour #04", None, None) + CID_COLOUR05 = (0x118, TID_COMMAND, "Colour #05", "Colour #05", None, None) + CID_COLOUR06 = (0x119, TID_COMMAND, "Colour #06", "Colour #06", None, None) + CID_COLOUR07 = (0x11a, TID_COMMAND, "Colour #07", "Colour #07", None, None) + CID_COLOUR08 = (0x11b, TID_COMMAND, "Colour #08", "Colour #08", None, None) + CID_COLOUR09 = (0x11c, TID_COMMAND, "Colour #09", "Colour #09", None, None) + CID_COLOUR10 = (0x11d, TID_COMMAND, "Colour #10", "Colour #10", None, None) + CID_COLOUR11 = (0x11e, TID_COMMAND, "Colour #11", "Colour #11", None, None) + CID_COLOUR12 = (0x11f, TID_COMMAND, "Colour #12", "Colour #12", None, None) + CID_COLOUR13 = (0x120, TID_COMMAND, "Colour #13", "Colour #13", None, None) + CID_COLOUR14 = (0x121, TID_COMMAND, "Colour #14", "Colour #14", None, None) + CID_COLOUR15 = (0x122, TID_COMMAND, "Colour #15", "Colour #15", None, None) # }}} # {{{ Non-items NID_MENU_SEP = (0x200, TID_NOTHING) @@ -107,79 +105,61 @@ class MiRCARTFrame(wx.Frame): CID_COLOUR15)) # }}} # {{{ Accelerators (hotkeys) - AID_EDIT = (0x500, TID_ACCELS, (CID_UNDO, CID_REDO)) + AID_EDIT = (0x500, TID_ACCELS, ( \ + CID_NEW, CID_OPEN, CID_SAVE, CID_UNDO, CID_REDO)) # }}} - # {{{ _drawIcon(self, solidColour): XXX - def _drawIcon(self, solidColour): - iconBitmap = wx.Bitmap((16,16)) - iconDc = wx.MemoryDC(); iconDc.SelectObject(iconBitmap); - iconBrush = wx.Brush(wx.Colour(solidColour), wx.BRUSHSTYLE_SOLID) - iconDc.SetBrush(iconBrush); iconDc.SetBackground(iconBrush); - iconDc.SetPen(wx.Pen(wx.Colour(solidColour), 1)) - iconDc.DrawRectangle(0, 0, 16, 16) - return iconBitmap + # {{{ _dialogSaveChanges(self) + def _dialogSaveChanges(self): + with wx.MessageDialog(self, \ + "Do you want to save changes to {}?".format( \ + self.canvasPathName), "MiRCART", \ + wx.CANCEL|wx.CANCEL_DEFAULT|wx.ICON_QUESTION|wx.YES_NO) as dialog: + dialogChoice = dialog.ShowModal() + return dialogChoice # }}} - # {{{ _initAccelTable(self, accelsDescr, handler): XXX - def _initAccelTable(self, accelsDescr, handler): - accelTableEntries = [wx.AcceleratorEntry() for n in range(0, len(accelsDescr[2]))] - for numAccel in range(0, len(accelsDescr[2])): - accelDescr = accelsDescr[2][numAccel] - if accelDescr[5] != None: - accelTableEntries[numAccel].Set(accelDescr[5][0], accelDescr[5][1], accelDescr[0]) - self.Bind(wx.EVT_MENU, handler, id=accelDescr[0]) - return accelTableEntries + # {{{ _setPaletteToolBitmaps(self): XXX + def _setPaletteToolBitmaps(self): + paletteDescr = ( \ + self.CID_COLOUR00, self.CID_COLOUR01, self.CID_COLOUR02, self.CID_COLOUR03, self.CID_COLOUR04, \ + self.CID_COLOUR05, self.CID_COLOUR06, self.CID_COLOUR07, self.CID_COLOUR08, self.CID_COLOUR09, \ + self.CID_COLOUR10, self.CID_COLOUR11, self.CID_COLOUR12, self.CID_COLOUR13, self.CID_COLOUR14, \ + self.CID_COLOUR15) + for numColour in range(len(paletteDescr)): + toolBitmapColour = MiRCARTColours[numColour][0:4] + toolBitmap = wx.Bitmap((16,16)) + toolBitmapDc = wx.MemoryDC(); toolBitmapDc.SelectObject(toolBitmap); + toolBitmapBrush = wx.Brush( \ + wx.Colour(toolBitmapColour), wx.BRUSHSTYLE_SOLID) + toolBitmapDc.SetBrush(toolBitmapBrush) + toolBitmapDc.SetBackground(toolBitmapBrush) + toolBitmapDc.SetPen(wx.Pen(wx.Colour(toolBitmapColour), 1)) + toolBitmapDc.DrawRectangle(0, 0, 16, 16) + self.toolBar.SetToolNormalBitmap( \ + paletteDescr[numColour][0], toolBitmap) # }}} - # {{{ _initMenus(self, menuBar, menusDescr, handler): XXX - def _initMenus(self, menuBar, menusDescr, handler): - for menuDescr in menusDescr: - menuWindow = wx.Menu() - for menuItem in menuDescr[4]: - if menuItem == self.NID_MENU_SEP: - menuWindow.AppendSeparator() - elif menuItem[1] == self.TID_SELECT: - menuItemWindow = menuWindow.AppendRadioItem(menuItem[0], menuItem[3], menuItem[2]) - self.menuItemsById[menuItem[0]] = menuItemWindow - self.Bind(wx.EVT_MENU, handler, menuItemWindow) - if len(menuItem) == 7: - menuItemWindow.Check(menuItem[6]) - else: - menuItemWindow = menuWindow.Append(menuItem[0], menuItem[3], menuItem[2]) - self.menuItemsById[menuItem[0]] = menuItemWindow - self.Bind(wx.EVT_MENU, handler, menuItemWindow) - if len(menuItem) == 7: - menuItemWindow.Enable(menuItem[6]) - menuBar.Append(menuWindow, menuDescr[3]) - # }}} - # {{{ _initToolBars(self, toolBar, toolBarsDescr, handler): XXX - def _initToolBars(self, toolBar, toolBarsDescr, handler): - for toolBarDescr in toolBarsDescr: - for toolBarItem in toolBarDescr[2]: - if toolBarItem == self.NID_TOOLBAR_SEP: - toolBar.AddSeparator() - else: - if len(toolBarItem[4]) == 4: - toolBarItemIcon = self._drawIcon(toolBarItem[4]) - elif len(toolBarItem[4]) == 1 \ - and toolBarItem[4][0] != None: - toolBarItemIcon = wx.ArtProvider.GetBitmap( \ - toolBarItem[4][0], wx.ART_TOOLBAR, (16,16)) - else: - toolBarItemIcon = wx.ArtProvider.GetBitmap( \ - wx.ART_HELP, wx.ART_TOOLBAR, (16,16)) - toolBarItemWindow = self.toolBar.AddTool( \ - toolBarItem[0], toolBarItem[2], toolBarItemIcon) - self.Bind(wx.EVT_TOOL, handler, toolBarItemWindow) - self.Bind(wx.EVT_TOOL_RCLICKED, handler, toolBarItemWindow) - # }}} - # {{{ _updateStatusBar(self): XXX - def _updateStatusBar(self): - text = "Foreground colour:" - text += " " + str(self.panelCanvas.mircFg) - text += " | " - text += "Background colour:" - text += " " + str(self.panelCanvas.mircBg) - self.statusBar.SetStatusText(text) + # {{{ _updateStatusBar(self, showColours=None, showFileName=True, showPos=None): XXX + def _updateStatusBar(self, showColours=True, showFileName=True, showPos=True): + if showColours == True: + showColours = self.panelCanvas.brushColours + if showPos == True: + showPos = self.panelCanvas.brushPos + if showFileName == True: + showFileName = self.canvasPathName + textItems = [] + if showPos != None: + textItems.append("X: {:03d} Y: {:03d}".format( \ + showPos[0], showPos[1])) + if showColours != None: + textItems.append("FG: {:02d}, BG: {:02d}".format( \ + showColours[0],showColours[1])) + textItems.append("{} on {}".format( \ + MiRCARTColours[showColours[0]][4], \ + MiRCARTColours[showColours[1]][4])) + if showFileName != None: + textItems.append("Current file: {}".format( \ + os.path.basename(showFileName))) + self.statusBar.SetStatusText(" | ".join(textItems)) # }}} # {{{ canvasExportAsPng(self): XXX @@ -189,31 +169,57 @@ class MiRCARTFrame(wx.Frame): if dialog.ShowModal() == wx.ID_CANCEL: return False else: - try: - outPathName = dialog.GetPath() - self.panelCanvas.exportPngFile(outhPathName) - return True - except IOError as error: - pass + outPathName = dialog.GetPath() + self.panelCanvas.canvasStore.exportBitmapToPngFile( \ + self.panelCanvas.canvasBitmap, outPathName, \ + wx.BITMAP_TYPE_PNG) + return True # }}} # {{{ canvasExportPastebin(self): XXX def canvasExportPastebin(self): - try: - self.panelCanvas.exportPastebin("") - return True - except IOError as error: - pass + pasteStatus, pasteResult = \ + self.panelCanvas.canvasStore.exportPastebin( \ + "", \ + self.panelCanvas.canvasMap, \ + self.panelCanvas.canvasSize) + if pasteStatus: + if not wx.TheClipboard.IsOpened(): + wx.TheClipboard.Open() + wx.TheClipboard.SetData(wx.TextDataObject(pasteResult)) + wx.TheClipboard.Close() + wx.MessageBox("Exported to Pastebin: " + pasteResult, \ + "Export to Pastebin", wx.OK|wx.ICON_INFORMATION) + else: + wx.MessageBox("Failed to export to Pastebin: " + pasteResult, \ + "Export to Pastebin", wx.OK|wx.ICON_EXCLAMATION) # }}} # {{{ canvasNew(self, newCanvasSize=None): XXX def canvasNew(self, newCanvasSize=None): + if self.canvasPathName != None: + saveChanges = self._dialogSaveChanges() + if saveChanges == wx.ID_CANCEL: + return + elif saveChanges == wx.ID_NO: + pass + elif saveChanges == wx.ID_YES: + self.canvasSave() if newCanvasSize == None: newCanvasSize = (100, 30) self.panelCanvas.canvasStore.importNew(newCanvasSize) + self.canvasPathName = None self._updateStatusBar(); self.onCanvasUpdate(); # }}} # {{{ canvasOpen(self): XXX def canvasOpen(self): - with wx.FileDialog(self, self.CID_OPEN[2], os.getcwd(), "", \ + if self.canvasPathName != None: + saveChanges = self._dialogSaveChanges() + if saveChanges == wx.ID_CANCEL: + return + elif saveChanges == wx.ID_NO: + pass + elif saveChanges == wx.ID_YES: + self.canvasSave() + with wx.FileDialog(self, self.CID_OPEN[2], os.getcwd(), "", \ "*.txt", wx.FD_OPEN) as dialog: if dialog.ShowModal() == wx.ID_CANCEL: return False @@ -230,10 +236,13 @@ class MiRCARTFrame(wx.Frame): if self.canvasSaveAs() == False: return try: - self.panelCanvas.exportTextFile(self.canvasPathName) - return True + with open(self.canvasPathName, "w") as outFile: + self.panelCanvas.canvasStore.exportTextFile( \ + self.panelCanvas.canvasMap, \ + self.panelCanvas.canvasSize, outFile) + return True except IOError as error: - pass + return False # }}} # {{{ canvasSaveAs(self): XXX def canvasSaveAs(self): @@ -243,22 +252,32 @@ class MiRCARTFrame(wx.Frame): return False else: self.canvasPathName = dialog.GetPath() - return True + return self.canvasSave() + # }}} + # {{{ onCanvasMotion(self, event): XXX + def onCanvasMotion(self, event, mapPoint=None): + eventType = event.GetEventType() + if eventType == wx.wxEVT_ENTER_WINDOW: + pass + elif eventType == wx.wxEVT_MOTION: + self._updateStatusBar(showPos=mapPoint) + elif eventType == wx.wxEVT_LEAVE_WINDOW: + pass # }}} # {{{ onCanvasUpdate(self): XXX def onCanvasUpdate(self): if self.panelCanvas.canvasJournal.patchesUndo[self.panelCanvas.canvasJournal.patchesUndoLevel] != None: self.menuItemsById[self.CID_UNDO[0]].Enable(True) + self.toolBar.EnableTool(self.CID_UNDO[0], True) else: self.menuItemsById[self.CID_UNDO[0]].Enable(False) + self.toolBar.EnableTool(self.CID_UNDO[0], False) if self.panelCanvas.canvasJournal.patchesUndoLevel > 0: self.menuItemsById[self.CID_REDO[0]].Enable(True) + self.toolBar.EnableTool(self.CID_REDO[0], True) else: self.menuItemsById[self.CID_REDO[0]].Enable(False) - # }}} - # {{{ onClose(self, event): XXX - def onClose(self, event): - self.Destroy(); self.__del__(); + self.toolBar.EnableTool(self.CID_REDO[0], False) # }}} # {{{ onFrameCommand(self, event): XXX def onFrameCommand(self, event): @@ -305,9 +324,9 @@ class MiRCARTFrame(wx.Frame): and cid <= self.CID_COLOUR15[0]: numColour = cid - self.CID_COLOUR00[0] if event.GetEventType() == wx.wxEVT_TOOL: - self.panelCanvas.mircFg = numColour + self.panelCanvas.brushColours[0] = numColour elif event.GetEventType() == wx.wxEVT_TOOL_RCLICKED: - self.panelCanvas.mircBg = numColour + self.panelCanvas.brushColours[1] = numColour self._updateStatusBar() # }}} # {{{ __del__(self): destructor method @@ -319,32 +338,13 @@ class MiRCARTFrame(wx.Frame): # # __init__(self, parent, appSize=(800, 600), canvasPos=(25, 50), cellSize=(7, 14), canvasSize=(100, 30), canvasTools=[]): initialisation method def __init__(self, parent, appSize=(800, 600), canvasPos=(25, 50), cellSize=(7, 14), canvasSize=(100, 30), canvasTools=[]): - super().__init__(parent, wx.ID_ANY, "MiRCART", size=appSize) - self.panelSkin = wx.Panel(self, wx.ID_ANY) + panelSkin = super().__init__(parent, wx.ID_ANY, "MiRCART", size=appSize) + self._setPaletteToolBitmaps() self.canvasPos = canvasPos; self.cellSize = cellSize; self.canvasSize = canvasSize; self.canvasPathName = None - - self.menuItemsById = {}; self.menuBar = wx.MenuBar(); - self._initMenus(self.menuBar, \ - [self.MID_FILE, self.MID_EDIT, self.MID_TOOLS], self.onFrameCommand) - self.SetMenuBar(self.menuBar) - - self.toolBar = wx.ToolBar(self.panelSkin, -1, \ - style=wx.HORIZONTAL|wx.TB_FLAT|wx.TB_NODIVIDER) - self.toolBar.SetToolBitmapSize((16,16)) - self._initToolBars(self.toolBar, [self.BID_TOOLBAR], self.onFrameCommand) - self.toolBar.Realize(); self.toolBar.Fit(); - - self.accelTable = wx.AcceleratorTable( \ - self._initAccelTable(self.AID_EDIT, self.onFrameCommand)) - self.SetAcceleratorTable(self.accelTable) - - self.Bind(wx.EVT_CLOSE, self.onClose) - self.statusBar = self.CreateStatusBar(); - self.SetFocus(); self.Show(True); self.canvasTools = canvasTools - self.panelCanvas = MiRCARTCanvas(self.panelSkin, parentFrame=self, \ - canvasPos=self.canvasPos, canvasSize=self.canvasSize, \ + self.panelCanvas = MiRCARTCanvas(panelSkin, parentFrame=self, \ + canvasPos=self.canvasPos, canvasSize=self.canvasSize, \ canvasTools=self.canvasTools, cellSize=self.cellSize) self.canvasNew() diff --git a/MiRCARTGeneralFrame.py b/MiRCARTGeneralFrame.py new file mode 100644 index 0000000..798af47 --- /dev/null +++ b/MiRCARTGeneralFrame.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +# +# MiRCARTGeneralFrame.py -- XXX +# Copyright (c) 2018 Lucio Andrés Illanes Albornoz +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +import os, wx + +# +# Types +TID_COMMAND = (0x001) +TID_MENU = (0x002) +TID_NOTHING = (0x003) +TID_SELECT = (0x004) +TID_TOOLBAR = (0x005) +TID_ACCELS = (0x006) + +class MiRCARTGeneralFrame(wx.Frame): + """XXX""" + menuItemsById = toolBarItemsById = None + statusBar = toolBar = None + + # {{{ _initAccelTable(self, accelsDescr, handler): XXX + def _initAccelTable(self, accelsDescr, handler): + accelTableEntries = [wx.AcceleratorEntry() for n in range(len(accelsDescr[2]))] + for numAccel in range(len(accelsDescr[2])): + accelDescr = accelsDescr[2][numAccel] + if accelDescr[5] != None: + accelTableEntries[numAccel].Set(*accelDescr[5], accelDescr[0]) + self.Bind(wx.EVT_MENU, handler, id=accelDescr[0]) + return accelTableEntries + # }}} + # {{{ _initMenus(self, menuBar, menusDescr, handler): XXX + def _initMenus(self, menuBar, menusDescr, handler): + for menuDescr in menusDescr: + menuWindow = wx.Menu() + for menuItem in menuDescr[4]: + if menuItem == self.NID_MENU_SEP: + menuWindow.AppendSeparator() + elif menuItem[1] == TID_SELECT: + menuItemWindow = menuWindow.AppendRadioItem(menuItem[0], menuItem[3], menuItem[2]) + self.menuItemsById[menuItem[0]] = menuItemWindow + self.Bind(wx.EVT_MENU, handler, menuItemWindow) + if len(menuItem) == 7: + menuItemWindow.Check(menuItem[6]) + else: + menuItemWindow = menuWindow.Append(menuItem[0], menuItem[3], menuItem[2]) + self.menuItemsById[menuItem[0]] = menuItemWindow + self.Bind(wx.EVT_MENU, handler, menuItemWindow) + if len(menuItem) == 7: + menuItemWindow.Enable(menuItem[6]) + menuBar.Append(menuWindow, menuDescr[3]) + # }}} + # {{{ _initToolBars(self, toolBar, toolBarsDescr, handler): XXX + def _initToolBars(self, toolBar, toolBarsDescr, handler): + for toolBarDescr in toolBarsDescr: + for toolBarItem in toolBarDescr[2]: + if toolBarItem == self.NID_TOOLBAR_SEP: + toolBar.AddSeparator() + else: + if toolBarItem[4] != None: + toolBarItemIcon = wx.ArtProvider.GetBitmap( \ + toolBarItem[4], wx.ART_TOOLBAR, (16,16)) + else: + toolBarItemIcon = wx.ArtProvider.GetBitmap( \ + wx.ART_HELP, wx.ART_TOOLBAR, (16,16)) + toolBarItemWindow = self.toolBar.AddTool( \ + toolBarItem[0], toolBarItem[2], toolBarItemIcon) + self.toolBarItemsById[toolBarItem[0]] = toolBarItemWindow + if len(toolBarItem) == 7 \ + and toolBarItem[1] == TID_COMMAND: + toolBarItemWindow.Enable(toolBarItem[6]) + self.Bind(wx.EVT_TOOL, handler, toolBarItemWindow) + self.Bind(wx.EVT_TOOL_RCLICKED, handler, toolBarItemWindow) + # }}} + # {{{ onClose(self, event): XXX + def onClose(self, event): + self.Destroy(); self.__del__(); + # }}} + # {{{ onFrameCommand(self, event): XXX + def onFrameCommand(self, event): + pass + # }}} + + # + # __init__(self, *args, **kwargs): initialisation method + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + panelSkin = wx.Panel(self, wx.ID_ANY) + + # Initialise menu bar, menus & menu items + self.menuItemsById = {}; menuBar = wx.MenuBar(); + self._initMenus(menuBar, \ + [self.MID_FILE, self.MID_EDIT, self.MID_TOOLS], + self.onFrameCommand) + self.SetMenuBar(menuBar) + + # Initialise toolbar & toolbar items + self.toolBarItemsById = {} + self.toolBar = wx.ToolBar(panelSkin, -1, \ + style=wx.HORIZONTAL|wx.TB_FLAT|wx.TB_NODIVIDER) + self.toolBar.SetToolBitmapSize((16,16)) + self._initToolBars(self.toolBar, [self.BID_TOOLBAR], self.onFrameCommand) + self.toolBar.Realize(); self.toolBar.Fit(); + + # Initialise accelerators (hotkeys) + accelTable = wx.AcceleratorTable( \ + self._initAccelTable(self.AID_EDIT, self.onFrameCommand)) + self.SetAcceleratorTable(accelTable) + + # Initialise status bar + self.statusBar = self.CreateStatusBar() + + # Bind event handlers + self.Bind(wx.EVT_CLOSE, self.onClose) + + # Set focus on & show window + self.SetFocus(); self.Show(True); + + return panelSkin + +# vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/MiRCARTToPngFile.py b/MiRCARTToPngFile.py index 8503b41..cf6e7de 100755 --- a/MiRCARTToPngFile.py +++ b/MiRCARTToPngFile.py @@ -22,13 +22,13 @@ # SOFTWARE. # +import MiRCARTCanvasStore from PIL import Image, ImageDraw, ImageFont -from MiRCARTFromTextFile import MiRCARTFromTextFile import string, sys class MiRCARTToPngFile: """XXX""" - inFile = inFilePath = inFromTextFile = None + inFile = inFromTextFile = None outFontFilePath = outFontSize = None # {{{ _ColourMapBold: mIRC colour number to RGBA map given ^B (bold) @@ -81,23 +81,23 @@ class MiRCARTToPngFile: # }}} # {{{ export(self, outFilePath): XXX def export(self, outFilePath): - inSize = self.inFromTextFile.getSize(); - outSize = [inSize[n] * self.outImgFontSize[n] for n in range(0, 2)] + inSize = (len(self.inCanvasMap[0]), len(self.inCanvasMap)) + outSize = [a*b for a,b in zip(inSize, self.outImgFontSize)] outCurPos = [0, 0] outImg = Image.new("RGBA", outSize, (*self._ColourMapNormal[1], 255)) outImgDraw = ImageDraw.Draw(outImg) - for inCurRow in range(0, inSize[1]): - for inCurCol in range(0, len(self.inFromTextFile.outMap[inCurRow])): - inCurCell = self.inFromTextFile.outMap[inCurRow][inCurCol] + for inCurRow in range(len(self.inCanvasMap)): + for inCurCol in range(len(self.inCanvasMap[inCurRow])): + inCurCell = self.inCanvasMap[inCurRow][inCurCol] outColours = [0, 0] - if inCurCell[1] & MiRCARTFromTextFile._CellState.CS_BOLD: + if inCurCell[1] & 0x02: if inCurCell[2] != " ": outColours[0] = self._ColourMapBold[inCurCell[0][0]] outColours[1] = self._ColourMapBold[inCurCell[0][1]] else: if inCurCell[2] != " ": outColours[0] = self._ColourMapNormal[inCurCell[0][0]] - outColours[1] = self._ColourMapNormal[inCurCell[0][1]] + outColours[1] = self._ColourMapNormal[inCurCell[0][1]] outImgDraw.rectangle((*outCurPos, \ outCurPos[0] + self.outImgFontSize[0], \ outCurPos[1] + self.outImgFontSize[1]), \ @@ -107,7 +107,7 @@ class MiRCARTToPngFile: # XXX implement italic outImgDraw.text(outCurPos, \ inCurCell[2], (*outColours[0], 255), self.outImgFont) - if inCurCell[1] & MiRCARTFromTextFile._CellState.CS_UNDERLINE: + if inCurCell[1] & 0x1f: self._drawUnderLine(curPos, self.outImgFontSize, outImgDraw) outCurPos[0] += self.outImgFontSize[0]; outCurPos[0] = 0 @@ -116,11 +116,9 @@ class MiRCARTToPngFile: # }}} # - # __init__(self, inFilePath, fontFilePath="DejaVuSansMono.ttf", fontSize=11): initialisation method - def __init__(self, inFilePath, fontFilePath="DejaVuSansMono.ttf", fontSize=11): - self.inFilePath = inFilePath; self.inFile = open(inFilePath, "r"); - self.inFromTextFile = MiRCARTFromTextFile(self.inFile) - + # __init__(self, inCanvasMap, fontFilePath="DejaVuSansMono.ttf", fontSize=11): initialisation method + def __init__(self, inCanvasMap, fontFilePath="DejaVuSansMono.ttf", fontSize=11): + self.inCanvasMap = inCanvasMap self.outFontFilePath = fontFilePath; self.outFontSize = int(fontSize); self.outImgFont = ImageFont.truetype( \ self.outFontFilePath, self.outFontSize) @@ -130,7 +128,8 @@ class MiRCARTToPngFile: # # Entry point def main(*argv): - MiRCARTToPngFile(argv[1], *argv[3:]).export(argv[2]) + canvasStore = MiRCARTCanvasStore.MiRCARTCanvasStore(inFile=argv[1]) + MiRCARTToPngFile(canvasStore.outMap, *argv[3:]).export(argv[2]) if __name__ == "__main__": if ((len(sys.argv) - 1) < 2)\ or ((len(sys.argv) - 1) > 4): diff --git a/MiRCARTTool.py b/MiRCARTTool.py index 0dad5ad..ae91e6d 100644 --- a/MiRCARTTool.py +++ b/MiRCARTTool.py @@ -26,8 +26,8 @@ class MiRCARTTool(): """XXX""" parentCanvas = None - # {{{ onMouseEvent(self, event, mapPoint, isDragging, isLeftDown, isRightDown): XXX - def onMouseEvent(self, event, mapPoint, isDragging, isLeftDown, isRightDown): + # {{{ onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown): XXX + def onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown): pass # }}} diff --git a/MiRCARTToolRect.py b/MiRCARTToolRect.py index b9e9fb2..71e226d 100644 --- a/MiRCARTToolRect.py +++ b/MiRCARTToolRect.py @@ -28,25 +28,19 @@ class MiRCARTToolRect(MiRCARTTool): """XXX""" # - # onMouseEvent(self, event, mapPoint, isDragging, isLeftDown, isRightDown): XXX - def onMouseEvent(self, event, mapPoint, isDragging, isLeftDown, isRightDown): + # onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown): XXX + def onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown): + brushColours = brushColours.copy() if isLeftDown: - return [[False, [[0, 0, \ - self.parentCanvas.mircFg, \ - self.parentCanvas.mircFg, " "]]], - [True, [[0, 0, \ - self.parentCanvas.mircFg, \ - self.parentCanvas.mircFg, " "]]]] + brushColours[1] = brushColours[0] + return [[False, [[[0, 0], brushColours, 0, " "]]], \ + [True, [[[0, 0], brushColours, 0, " "]]]] elif isRightDown: - return [[False, [[0, 0, \ - self.parentCanvas.mircBg, \ - self.parentCanvas.mircBg, " "]]], \ - [True, [[0, 0, \ - self.parentCanvas.mircBg, \ - self.parentCanvas.mircBg, " "]]]] + brushColours[0] = brushColours[1] + return [[False, [[[0, 0], brushColours, 0, " "]]], \ + [True, [[[0, 0], brushColours, 0, " "]]]] else: - return [[True, [[0, 0, \ - self.parentCanvas.mircFg, \ - self.parentCanvas.mircFg, " "]]]] + brushColours[1] = brushColours[0] + return [[True, [[[0, 0], brushColours, 0, " "]]]] # vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/README.md b/README.md index 5785840..2795ff8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MiRCART.py -- mIRC art editor for Windows & Linux (WIP) -* Prerequisites on Windows: install Python v3.6.x[1] and wxPython v4.x.x w/ the following elevated command prompt command line: - `pip install wxPython` +* Prerequisites on Windows: install Python v3.6.x[1] and script dependencies w/ the following elevated command prompt command line: + `pip install requests urllib3 wxPython` * Prerequisites on Linux: python3 && python-wx{gtk2.8,tools} on Debian-family Linux distributions * Screenshot: ![Screenshot](https://github.com/lalbornoz/MiRCARTools/raw/master/MiRCART.png "Screenshot") From 39babc71a4b8cd75402c3a5004da94652006273b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Sun, 7 Jan 2018 23:49:57 +0100 Subject: [PATCH 077/148] MiRCART{CanvasStore,Frame}.py: implements export to Imgur (from bitmap via PNG.) --- MiRCARTCanvasStore.py | 46 ++++++++++++++++++----- MiRCARTFrame.py | 87 ++++++++++++++++++++++++++----------------- 2 files changed, 90 insertions(+), 43 deletions(-) diff --git a/MiRCARTCanvasStore.py b/MiRCARTCanvasStore.py index 8a65cd2..e41b35a 100644 --- a/MiRCARTCanvasStore.py +++ b/MiRCARTCanvasStore.py @@ -22,7 +22,7 @@ # SOFTWARE. # -import io +import io, os, tempfile try: import wx @@ -37,7 +37,7 @@ except ImportError: haveMiRCARTToPngFile = False try: - import requests, urllib.request + import base64, json, requests, urllib.request haveUrllib = True except ImportError: haveUrllib = False @@ -62,6 +62,32 @@ class MiRCARTCanvasStore(): PS_COLOUR_DIGIT0 = 2 PS_COLOUR_DIGIT1 = 3 + # {{{ _exportFileToImgur(self, apiKey, imgName, imgTitle, pathName): upload single PNG file to Imgur + def _exportFileToImgur(self, apiKey, imgName, imgTitle, pathName): + requestImageData = open(pathName, "rb").read() + requestData = { \ + "image": base64.b64encode(requestImageData), \ + "key": apiKey, \ + "name": imgName, \ + "title": imgTitle, \ + "type": "base64"} + requestHeaders = {"Authorization": "Client-ID " + apiKey} + responseHttp = requests.post( \ + "https://api.imgur.com/3/upload.json", \ + data=requestData, headers=requestHeaders) + responseDict = json.loads(responseHttp.text) + if responseHttp.status_code == 200: + return [200, responseDict.get("data").get("link")] + else: + return [responseHttp.status_code, ""] + # }}} + # {{{ _flipCellStateBit(self, cellState, bit): XXX + def _flipCellStateBit(self, cellState, bit): + if cellState & bit: + return cellState & ~bit + else: + return cellState | bit + # }}} # {{{ _parseCharAsColourSpec(self, colourSpec, curColours): XXX def _parseCharAsColourSpec(self, colourSpec, curColours): if len(colourSpec) > 0: @@ -74,18 +100,20 @@ class MiRCARTCanvasStore(): else: return (15, 1) # }}} - # {{{ _flipCellStateBit(self, cellState, bit): XXX - def _flipCellStateBit(self, cellState, bit): - if cellState & bit: - return cellState & ~bit - else: - return cellState | bit - # }}} # {{{ exportBitmapToPngFile(self, canvasBitmap, outPathName, outType): XXX def exportBitmapToPngFile(self, canvasBitmap, outPathName, outType): return canvasBitmap.ConvertToImage().SaveFile(outPathName, outType) # }}} + # {{{ exportBitmapToImgur(self, apiKey, canvasBitmap, imgName, imgTitle, imgType): XXX + def exportBitmapToImgur(self, apiKey, canvasBitmap, imgName, imgTitle, imgType): + tmpPathName = tempfile.mkstemp() + os.close(tmpPathName[0]) + canvasBitmap.ConvertToImage().SaveFile(tmpPathName[1], imgType) + imgurResult = self._exportFileToImgur(apiKey, imgName, imgTitle, tmpPathName[1]) + os.remove(tmpPathName[1]) + return imgurResult + # }}} # {{{ exportPastebin(self, apiDevKey, canvasMap, canvasSize, pasteName="", pastePrivate=0): XXX def exportPastebin(self, apiDevKey, canvasMap, canvasSize, pasteName="", pastePrivate=0): if haveUrllib: diff --git a/MiRCARTFrame.py b/MiRCARTFrame.py index 79af864..dfc0324 100644 --- a/MiRCARTFrame.py +++ b/MiRCARTFrame.py @@ -42,39 +42,41 @@ class MiRCARTFrame(MiRCARTGeneralFrame): CID_SAVEAS = (0x103, TID_COMMAND, "Save As...", "Save &As...", wx.ART_FILE_SAVE_AS, None) CID_EXPORT_AS_PNG = (0x104, TID_COMMAND, "Export as PNG...", \ "Export as PN&G...", None, None) - CID_EXPORT_PASTEBIN = (0x105, TID_COMMAND, "Export to Pastebin...", \ + CID_EXPORT_IMGUR = (0x105, TID_COMMAND, "Export to Imgur...", \ + "Export to I&mgur...", None, None, haveUrllib) + CID_EXPORT_PASTEBIN = (0x106, TID_COMMAND, "Export to Pastebin...", \ "Export to Pasteb&in...", None, None, haveUrllib) - CID_EXIT = (0x106, TID_COMMAND, "Exit", "E&xit", None, None) - CID_UNDO = (0x107, TID_COMMAND, "Undo", "&Undo", wx.ART_UNDO, (wx.ACCEL_CTRL, ord("Z")), False) - CID_REDO = (0x108, TID_COMMAND, "Redo", "&Redo", wx.ART_REDO, (wx.ACCEL_CTRL, ord("Y")), False) - CID_CUT = (0x109, TID_COMMAND, "Cut", "Cu&t", wx.ART_CUT, None, False) - CID_COPY = (0x10a, TID_COMMAND, "Copy", "&Copy", wx.ART_COPY, None, False) - CID_PASTE = (0x10b, TID_COMMAND, "Paste", "&Paste", wx.ART_PASTE, None, False) - CID_DELETE = (0x10c, TID_COMMAND, "Delete", "De&lete", wx.ART_DELETE, None, False) - CID_INCRBRUSH = (0x10d, TID_COMMAND, "Increase brush size", \ + CID_EXIT = (0x107, TID_COMMAND, "Exit", "E&xit", None, None) + CID_UNDO = (0x108, TID_COMMAND, "Undo", "&Undo", wx.ART_UNDO, (wx.ACCEL_CTRL, ord("Z")), False) + CID_REDO = (0x109, TID_COMMAND, "Redo", "&Redo", wx.ART_REDO, (wx.ACCEL_CTRL, ord("Y")), False) + CID_CUT = (0x10a, TID_COMMAND, "Cut", "Cu&t", wx.ART_CUT, None, False) + CID_COPY = (0x10b, TID_COMMAND, "Copy", "&Copy", wx.ART_COPY, None, False) + CID_PASTE = (0x10c, TID_COMMAND, "Paste", "&Paste", wx.ART_PASTE, None, False) + CID_DELETE = (0x10d, TID_COMMAND, "Delete", "De&lete", wx.ART_DELETE, None, False) + CID_INCRBRUSH = (0x10e, TID_COMMAND, "Increase brush size", \ "&Increase brush size", wx.ART_PLUS, None) - CID_DECRBRUSH = (0x10e, TID_COMMAND, "Decrease brush size", \ + CID_DECRBRUSH = (0x10f, TID_COMMAND, "Decrease brush size", \ "&Decrease brush size", wx.ART_MINUS, None) - CID_SOLID_BRUSH = (0x10f, TID_SELECT, "Solid brush", "&Solid brush", None, None, True) - CID_RECT = (0x110, TID_SELECT, "Rectangle", "&Rectangle", None, None, True) - CID_CIRCLE = (0x111, TID_SELECT, "Circle", "&Circle", None, None, False) - CID_LINE = (0x112, TID_SELECT, "Line", "&Line", None, None, False) - CID_COLOUR00 = (0x113, TID_COMMAND, "Colour #00", "Colour #00", None, None) - CID_COLOUR01 = (0x114, TID_COMMAND, "Colour #01", "Colour #01", None, None) - CID_COLOUR02 = (0x115, TID_COMMAND, "Colour #02", "Colour #02", None, None) - CID_COLOUR03 = (0x116, TID_COMMAND, "Colour #03", "Colour #03", None, None) - CID_COLOUR04 = (0x117, TID_COMMAND, "Colour #04", "Colour #04", None, None) - CID_COLOUR05 = (0x118, TID_COMMAND, "Colour #05", "Colour #05", None, None) - CID_COLOUR06 = (0x119, TID_COMMAND, "Colour #06", "Colour #06", None, None) - CID_COLOUR07 = (0x11a, TID_COMMAND, "Colour #07", "Colour #07", None, None) - CID_COLOUR08 = (0x11b, TID_COMMAND, "Colour #08", "Colour #08", None, None) - CID_COLOUR09 = (0x11c, TID_COMMAND, "Colour #09", "Colour #09", None, None) - CID_COLOUR10 = (0x11d, TID_COMMAND, "Colour #10", "Colour #10", None, None) - CID_COLOUR11 = (0x11e, TID_COMMAND, "Colour #11", "Colour #11", None, None) - CID_COLOUR12 = (0x11f, TID_COMMAND, "Colour #12", "Colour #12", None, None) - CID_COLOUR13 = (0x120, TID_COMMAND, "Colour #13", "Colour #13", None, None) - CID_COLOUR14 = (0x121, TID_COMMAND, "Colour #14", "Colour #14", None, None) - CID_COLOUR15 = (0x122, TID_COMMAND, "Colour #15", "Colour #15", None, None) + CID_SOLID_BRUSH = (0x110, TID_SELECT, "Solid brush", "&Solid brush", None, None, True) + CID_RECT = (0x111, TID_SELECT, "Rectangle", "&Rectangle", None, None, True) + CID_CIRCLE = (0x112, TID_SELECT, "Circle", "&Circle", None, None, False) + CID_LINE = (0x113, TID_SELECT, "Line", "&Line", None, None, False) + CID_COLOUR00 = (0x114, TID_COMMAND, "Colour #00", "Colour #00", None, None) + CID_COLOUR01 = (0x115, TID_COMMAND, "Colour #01", "Colour #01", None, None) + CID_COLOUR02 = (0x116, TID_COMMAND, "Colour #02", "Colour #02", None, None) + CID_COLOUR03 = (0x117, TID_COMMAND, "Colour #03", "Colour #03", None, None) + CID_COLOUR04 = (0x118, TID_COMMAND, "Colour #04", "Colour #04", None, None) + CID_COLOUR05 = (0x119, TID_COMMAND, "Colour #05", "Colour #05", None, None) + CID_COLOUR06 = (0x11a, TID_COMMAND, "Colour #06", "Colour #06", None, None) + CID_COLOUR07 = (0x11b, TID_COMMAND, "Colour #07", "Colour #07", None, None) + CID_COLOUR08 = (0x11c, TID_COMMAND, "Colour #08", "Colour #08", None, None) + CID_COLOUR09 = (0x11d, TID_COMMAND, "Colour #09", "Colour #09", None, None) + CID_COLOUR10 = (0x11e, TID_COMMAND, "Colour #10", "Colour #10", None, None) + CID_COLOUR11 = (0x11f, TID_COMMAND, "Colour #11", "Colour #11", None, None) + CID_COLOUR12 = (0x120, TID_COMMAND, "Colour #12", "Colour #12", None, None) + CID_COLOUR13 = (0x121, TID_COMMAND, "Colour #13", "Colour #13", None, None) + CID_COLOUR14 = (0x122, TID_COMMAND, "Colour #14", "Colour #14", None, None) + CID_COLOUR15 = (0x123, TID_COMMAND, "Colour #15", "Colour #15", None, None) # }}} # {{{ Non-items NID_MENU_SEP = (0x200, TID_NOTHING) @@ -83,7 +85,7 @@ class MiRCARTFrame(MiRCARTGeneralFrame): # {{{ Menus MID_FILE = (0x300, TID_MENU, "File", "&File", ( \ CID_NEW, CID_OPEN, CID_SAVE, CID_SAVEAS, NID_MENU_SEP, \ - CID_EXPORT_PASTEBIN, CID_EXPORT_AS_PNG, NID_MENU_SEP, \ + CID_EXPORT_AS_PNG, CID_EXPORT_IMGUR, CID_EXPORT_PASTEBIN, NID_MENU_SEP, \ CID_EXIT)) MID_EDIT = (0x301, TID_MENU, "Edit", "&Edit", ( \ CID_UNDO, CID_REDO, NID_MENU_SEP, \ @@ -164,17 +166,32 @@ class MiRCARTFrame(MiRCARTGeneralFrame): # {{{ canvasExportAsPng(self): XXX def canvasExportAsPng(self): - with wx.FileDialog(self, self.CID_SAVEAS[2], os.getcwd(), "", \ + with wx.FileDialog(self, self.CID_SAVEAS[2], os.getcwd(), "", \ "*.png", wx.FD_SAVE|wx.FD_OVERWRITE_PROMPT) as dialog: if dialog.ShowModal() == wx.ID_CANCEL: return False else: outPathName = dialog.GetPath() - self.panelCanvas.canvasStore.exportBitmapToPngFile( \ - self.panelCanvas.canvasBitmap, outPathName, \ + self.panelCanvas.canvasStore.exportBitmapToPngFile( \ + self.panelCanvas.canvasBitmap, outPathName, \ wx.BITMAP_TYPE_PNG) return True # }}} + # {{{ canvasExportImgur(self): XXX + def canvasExportImgur(self): + imgurResult = self.panelCanvas.canvasStore.exportBitmapToImgur( \ + "c9a6efb3d7932fd", self.panelCanvas.canvasBitmap, "", "", wx.BITMAP_TYPE_PNG) + if imgurResult[0] == 200: + if not wx.TheClipboard.IsOpened(): + wx.TheClipboard.Open() + wx.TheClipboard.SetData(wx.TextDataObject(imgurResult[1])) + wx.TheClipboard.Close() + wx.MessageBox("Exported to Imgur: " + imgurResult[1], \ + "Export to Imgur", wx.OK|wx.ICON_INFORMATION) + else: + wx.MessageBox("Failed to export to Imgur: " + imgurResult[1], \ + "Export to Imgur", wx.OK|wx.ICON_EXCLAMATION) + # }}} # {{{ canvasExportPastebin(self): XXX def canvasExportPastebin(self): pasteStatus, pasteResult = \ @@ -292,6 +309,8 @@ class MiRCARTFrame(MiRCARTGeneralFrame): self.canvasSaveAs() elif cid == self.CID_EXPORT_AS_PNG[0]: self.canvasExportAsPng() + elif cid == self.CID_EXPORT_IMGUR[0]: + self.canvasExportImgur() elif cid == self.CID_EXPORT_PASTEBIN[0]: self.canvasExportPastebin() elif cid == self.CID_EXIT[0]: From 6a4356568bfd238fa06786787c126b8e3ab4d1f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Sun, 7 Jan 2018 23:55:26 +0100 Subject: [PATCH 078/148] MiRCARTFrame.py:MiRCARTFrame.canvas{Export{AsPng,Imgur,Pastebin},New,Open,Save}(): add wx.CURSOR_WAIT guards. --- MiRCARTFrame.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/MiRCARTFrame.py b/MiRCARTFrame.py index dfc0324..233fb00 100644 --- a/MiRCARTFrame.py +++ b/MiRCARTFrame.py @@ -172,15 +172,19 @@ class MiRCARTFrame(MiRCARTGeneralFrame): return False else: outPathName = dialog.GetPath() + self.SetCursor(wx.Cursor(wx.CURSOR_WAIT)) self.panelCanvas.canvasStore.exportBitmapToPngFile( \ self.panelCanvas.canvasBitmap, outPathName, \ wx.BITMAP_TYPE_PNG) + self.SetCursor(wx.Cursor(wx.NullCursor)) return True # }}} # {{{ canvasExportImgur(self): XXX def canvasExportImgur(self): + self.SetCursor(wx.Cursor(wx.CURSOR_WAIT)) imgurResult = self.panelCanvas.canvasStore.exportBitmapToImgur( \ "c9a6efb3d7932fd", self.panelCanvas.canvasBitmap, "", "", wx.BITMAP_TYPE_PNG) + self.SetCursor(wx.Cursor(wx.NullCursor)) if imgurResult[0] == 200: if not wx.TheClipboard.IsOpened(): wx.TheClipboard.Open() @@ -194,11 +198,13 @@ class MiRCARTFrame(MiRCARTGeneralFrame): # }}} # {{{ canvasExportPastebin(self): XXX def canvasExportPastebin(self): + self.SetCursor(wx.Cursor(wx.CURSOR_WAIT)) pasteStatus, pasteResult = \ self.panelCanvas.canvasStore.exportPastebin( \ "", \ self.panelCanvas.canvasMap, \ self.panelCanvas.canvasSize) + self.SetCursor(wx.Cursor(wx.NullCursor)) if pasteStatus: if not wx.TheClipboard.IsOpened(): wx.TheClipboard.Open() @@ -220,10 +226,12 @@ class MiRCARTFrame(MiRCARTGeneralFrame): pass elif saveChanges == wx.ID_YES: self.canvasSave() + self.SetCursor(wx.Cursor(wx.CURSOR_WAIT)) if newCanvasSize == None: newCanvasSize = (100, 30) self.panelCanvas.canvasStore.importNew(newCanvasSize) self.canvasPathName = None + self.SetCursor(wx.Cursor(wx.NullCursor)) self._updateStatusBar(); self.onCanvasUpdate(); # }}} # {{{ canvasOpen(self): XXX @@ -242,8 +250,10 @@ class MiRCARTFrame(MiRCARTGeneralFrame): return False else: self.canvasPathName = dialog.GetPath() + self.SetCursor(wx.Cursor(wx.CURSOR_WAIT)) self.panelCanvas.canvasStore.importTextFile(self.canvasPathName) self.panelCanvas.canvasStore.importIntoPanel() + self.SetCursor(wx.Cursor(wx.NullCursor)) self._updateStatusBar(); self.onCanvasUpdate(); return True # }}} @@ -254,9 +264,11 @@ class MiRCARTFrame(MiRCARTGeneralFrame): return try: with open(self.canvasPathName, "w") as outFile: + self.SetCursor(wx.Cursor(wx.CURSOR_WAIT)) self.panelCanvas.canvasStore.exportTextFile( \ self.panelCanvas.canvasMap, \ self.panelCanvas.canvasSize, outFile) + self.SetCursor(wx.Cursor(wx.NullCursor)) return True except IOError as error: return False From 54adf3a95c30f8c428cc0e2d54f5bce9458f5f7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Mon, 8 Jan 2018 00:25:59 +0100 Subject: [PATCH 079/148] MiRCART{CanvasJournal,Frame,ToolRect}.py: implements variable brush size. --- MiRCARTCanvas.py | 1 + MiRCARTCanvasJournal.py | 5 ++++- MiRCARTFrame.py | 13 +++++++++---- MiRCARTToolRect.py | 14 +++++++++----- 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/MiRCARTCanvas.py b/MiRCARTCanvas.py index 0503889..cb09834 100644 --- a/MiRCARTCanvas.py +++ b/MiRCARTCanvas.py @@ -221,6 +221,7 @@ class MiRCARTCanvas(wx.Panel): super().__init__(parent, pos=canvasPos, \ size=[w*h for w,h in zip(canvasSize, cellSize)]) + self.SetDoubleBuffered(True) self.Bind(wx.EVT_CLOSE, self.onClose) self.Bind(wx.EVT_ENTER_WINDOW, self.onMouseWindowEvent) self.Bind(wx.EVT_LEAVE_WINDOW, self.onMouseWindowEvent) diff --git a/MiRCARTCanvasJournal.py b/MiRCARTCanvasJournal.py index 3b70dbc..de9511d 100644 --- a/MiRCARTCanvasJournal.py +++ b/MiRCARTCanvasJournal.py @@ -60,7 +60,10 @@ class MiRCARTCanvasJournal(): self._popTmp(eventDc, tmpDc) for patch in mapPatch[1]: absMapPoint = self._relMapPointToAbsMapPoint(patch[0], atPoint) - if mapPatchTmp: + if absMapPoint[0] >= self.parentCanvas.canvasSize[0] \ + or absMapPoint[1] >= self.parentCanvas.canvasSize[1]: + continue + elif mapPatchTmp: self._pushTmp(absMapPoint) self.parentCanvas.onJournalUpdate(mapPatchTmp, \ absMapPoint, patch, eventDc, tmpDc, atPoint) diff --git a/MiRCARTFrame.py b/MiRCARTFrame.py index 233fb00..491be6a 100644 --- a/MiRCARTFrame.py +++ b/MiRCARTFrame.py @@ -340,9 +340,14 @@ class MiRCARTFrame(MiRCARTGeneralFrame): elif cid == self.CID_DELETE[0]: pass elif cid == self.CID_INCRBRUSH[0]: - pass - elif cid == self.CID_DECRBRUSH[0]: - pass + self.panelCanvas.brushSize = \ + [a+1 for a in self.panelCanvas.brushSize] + print(self.panelCanvas.brushSize) + elif cid == self.CID_DECRBRUSH[0] \ + and self.panelCanvas.brushSize != [0,0]: + self.panelCanvas.brushSize = \ + [a-1 for a in self.panelCanvas.brushSize] + print(self.panelCanvas.brushSize) elif cid == self.CID_SOLID_BRUSH[0]: pass elif cid == self.CID_RECT[0]: @@ -351,7 +356,7 @@ class MiRCARTFrame(MiRCARTGeneralFrame): pass elif cid == self.CID_LINE[0]: pass - elif cid >= self.CID_COLOUR00[0] \ + elif cid >= self.CID_COLOUR00[0] \ and cid <= self.CID_COLOUR15[0]: numColour = cid - self.CID_COLOUR00[0] if event.GetEventType() == wx.wxEVT_TOOL: diff --git a/MiRCARTToolRect.py b/MiRCARTToolRect.py index 71e226d..66a8864 100644 --- a/MiRCARTToolRect.py +++ b/MiRCARTToolRect.py @@ -33,14 +33,18 @@ class MiRCARTToolRect(MiRCARTTool): brushColours = brushColours.copy() if isLeftDown: brushColours[1] = brushColours[0] - return [[False, [[[0, 0], brushColours, 0, " "]]], \ - [True, [[[0, 0], brushColours, 0, " "]]]] elif isRightDown: brushColours[0] = brushColours[1] - return [[False, [[[0, 0], brushColours, 0, " "]]], \ - [True, [[[0, 0], brushColours, 0, " "]]]] else: brushColours[1] = brushColours[0] - return [[True, [[[0, 0], brushColours, 0, " "]]]] + brushPatches = [] + for brushRow in range(brushSize[1]): + for brushCol in range(brushSize[0] * 2): + brushPatches.append([[brushCol, brushRow], \ + brushColours, 0, " "]) + if isLeftDown or isRightDown: + return [[False, brushPatches], [True, brushPatches]] + else: + return [[True, brushPatches]] # vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 From da82a543b5ef307834245f1dbed047460304bc18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Mon, 8 Jan 2018 01:11:00 +0100 Subject: [PATCH 080/148] MiRCARTCanvas.py: disable double buffering. --- MiRCARTCanvas.py | 1 - 1 file changed, 1 deletion(-) diff --git a/MiRCARTCanvas.py b/MiRCARTCanvas.py index cb09834..0503889 100644 --- a/MiRCARTCanvas.py +++ b/MiRCARTCanvas.py @@ -221,7 +221,6 @@ class MiRCARTCanvas(wx.Panel): super().__init__(parent, pos=canvasPos, \ size=[w*h for w,h in zip(canvasSize, cellSize)]) - self.SetDoubleBuffered(True) self.Bind(wx.EVT_CLOSE, self.onClose) self.Bind(wx.EVT_ENTER_WINDOW, self.onMouseWindowEvent) self.Bind(wx.EVT_LEAVE_WINDOW, self.onMouseWindowEvent) From 30f3688a2cc44601006bf9827b3444db06efe5bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Mon, 8 Jan 2018 01:18:52 +0100 Subject: [PATCH 081/148] MiRCARTFrame.py:MiRCARTFrame.onFrameCommand(): don't decrease brush size below (1,1). MiRCARTFrame.py:MiRCARTFrame.onFrameCommand(): remove debugging print()s. --- MiRCARTFrame.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/MiRCARTFrame.py b/MiRCARTFrame.py index 491be6a..0988782 100644 --- a/MiRCARTFrame.py +++ b/MiRCARTFrame.py @@ -340,14 +340,13 @@ class MiRCARTFrame(MiRCARTGeneralFrame): elif cid == self.CID_DELETE[0]: pass elif cid == self.CID_INCRBRUSH[0]: - self.panelCanvas.brushSize = \ + self.panelCanvas.brushSize = \ [a+1 for a in self.panelCanvas.brushSize] - print(self.panelCanvas.brushSize) - elif cid == self.CID_DECRBRUSH[0] \ - and self.panelCanvas.brushSize != [0,0]: - self.panelCanvas.brushSize = \ + elif cid == self.CID_DECRBRUSH[0] \ + and self.panelCanvas.brushSize[0] > 1 \ + and self.panelCanvas.brushSize[1] > 1: + self.panelCanvas.brushSize = \ [a-1 for a in self.panelCanvas.brushSize] - print(self.panelCanvas.brushSize) elif cid == self.CID_SOLID_BRUSH[0]: pass elif cid == self.CID_RECT[0]: @@ -356,7 +355,7 @@ class MiRCARTFrame(MiRCARTGeneralFrame): pass elif cid == self.CID_LINE[0]: pass - elif cid >= self.CID_COLOUR00[0] \ + elif cid >= self.CID_COLOUR00[0] \ and cid <= self.CID_COLOUR15[0]: numColour = cid - self.CID_COLOUR00[0] if event.GetEventType() == wx.wxEVT_TOOL: From e7b7e05234dc6cb0ba11252f5d18c355285ff255 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Mon, 8 Jan 2018 01:43:54 +0100 Subject: [PATCH 082/148] MiRCARTGeneralFrame.py, MiRCARTFrame.py: cleanup. --- MiRCARTFrame.py | 7 +++- MiRCARTGeneralFrame.py | 83 +++++++++++++++++++++--------------------- 2 files changed, 48 insertions(+), 42 deletions(-) diff --git a/MiRCARTFrame.py b/MiRCARTFrame.py index 0988782..8f7dcd4 100644 --- a/MiRCARTFrame.py +++ b/MiRCARTFrame.py @@ -25,7 +25,7 @@ from MiRCARTCanvas import MiRCARTCanvas, haveUrllib from MiRCARTColours import MiRCARTColours from MiRCARTGeneralFrame import MiRCARTGeneralFrame, \ - TID_COMMAND, TID_MENU, TID_NOTHING, TID_SELECT, TID_TOOLBAR, TID_ACCELS + TID_ACCELS, TID_COMMAND, TID_LIST, TID_MENU, TID_NOTHING, TID_SELECT, TID_TOOLBAR import os, wx @@ -110,6 +110,11 @@ class MiRCARTFrame(MiRCARTGeneralFrame): AID_EDIT = (0x500, TID_ACCELS, ( \ CID_NEW, CID_OPEN, CID_SAVE, CID_UNDO, CID_REDO)) # }}} + # {{{ Lists + LID_ACCELS = (0x600, TID_LIST, (AID_EDIT)) + LID_MENUS = (0x601, TID_LIST, (MID_FILE, MID_EDIT, MID_TOOLS)) + LID_TOOLBARS = (0x602, TID_LIST, (BID_TOOLBAR)) + # }}} # {{{ _dialogSaveChanges(self) def _dialogSaveChanges(self): diff --git a/MiRCARTGeneralFrame.py b/MiRCARTGeneralFrame.py index 798af47..0c127e0 100644 --- a/MiRCARTGeneralFrame.py +++ b/MiRCARTGeneralFrame.py @@ -26,12 +26,13 @@ import os, wx # # Types -TID_COMMAND = (0x001) -TID_MENU = (0x002) -TID_NOTHING = (0x003) -TID_SELECT = (0x004) -TID_TOOLBAR = (0x005) -TID_ACCELS = (0x006) +TID_ACCELS = (0x001) +TID_COMMAND = (0x002) +TID_LIST = (0x003) +TID_MENU = (0x004) +TID_NOTHING = (0x005) +TID_SELECT = (0x006) +TID_TOOLBAR = (0x007) class MiRCARTGeneralFrame(wx.Frame): """XXX""" @@ -48,8 +49,9 @@ class MiRCARTGeneralFrame(wx.Frame): self.Bind(wx.EVT_MENU, handler, id=accelDescr[0]) return accelTableEntries # }}} - # {{{ _initMenus(self, menuBar, menusDescr, handler): XXX - def _initMenus(self, menuBar, menusDescr, handler): + # {{{ _initMenus(self, menusDescr, handler): XXX + def _initMenus(self, menusDescr, handler): + self.menuItemsById = {}; menuBar = wx.MenuBar(); for menuDescr in menusDescr: menuWindow = wx.Menu() for menuItem in menuDescr[4]: @@ -68,28 +70,33 @@ class MiRCARTGeneralFrame(wx.Frame): if len(menuItem) == 7: menuItemWindow.Enable(menuItem[6]) menuBar.Append(menuWindow, menuDescr[3]) + return menuBar # }}} - # {{{ _initToolBars(self, toolBar, toolBarsDescr, handler): XXX - def _initToolBars(self, toolBar, toolBarsDescr, handler): - for toolBarDescr in toolBarsDescr: - for toolBarItem in toolBarDescr[2]: - if toolBarItem == self.NID_TOOLBAR_SEP: - toolBar.AddSeparator() + # {{{ _initToolBars(self, toolBarsDescr, handler): XXX + def _initToolBars(self, toolBarsDescr, handler, panelSkin): + self.toolBarItemsById = {} + self.toolBar = wx.ToolBar(panelSkin, -1, \ + style=wx.HORIZONTAL|wx.TB_FLAT|wx.TB_NODIVIDER) + self.toolBar.SetToolBitmapSize((16,16)) + for toolBarItem in toolBarsDescr[2]: + if toolBarItem == self.NID_TOOLBAR_SEP: + self.toolBar.AddSeparator() + else: + if toolBarItem[4] != None: + toolBarItemIcon = wx.ArtProvider.GetBitmap( \ + toolBarItem[4], wx.ART_TOOLBAR, (16,16)) else: - if toolBarItem[4] != None: - toolBarItemIcon = wx.ArtProvider.GetBitmap( \ - toolBarItem[4], wx.ART_TOOLBAR, (16,16)) - else: - toolBarItemIcon = wx.ArtProvider.GetBitmap( \ - wx.ART_HELP, wx.ART_TOOLBAR, (16,16)) - toolBarItemWindow = self.toolBar.AddTool( \ - toolBarItem[0], toolBarItem[2], toolBarItemIcon) - self.toolBarItemsById[toolBarItem[0]] = toolBarItemWindow - if len(toolBarItem) == 7 \ - and toolBarItem[1] == TID_COMMAND: - toolBarItemWindow.Enable(toolBarItem[6]) - self.Bind(wx.EVT_TOOL, handler, toolBarItemWindow) - self.Bind(wx.EVT_TOOL_RCLICKED, handler, toolBarItemWindow) + toolBarItemIcon = wx.ArtProvider.GetBitmap( \ + wx.ART_HELP, wx.ART_TOOLBAR, (16,16)) + toolBarItemWindow = self.toolBar.AddTool( \ + toolBarItem[0], toolBarItem[2], toolBarItemIcon) + self.toolBarItemsById[toolBarItem[0]] = toolBarItemWindow + if len(toolBarItem) == 7 \ + and toolBarItem[1] == TID_COMMAND: + toolBarItemWindow.Enable(toolBarItem[6]) + self.Bind(wx.EVT_TOOL, handler, toolBarItemWindow) + self.Bind(wx.EVT_TOOL_RCLICKED, handler, toolBarItemWindow) + self.toolBar.Realize(); self.toolBar.Fit(); # }}} # {{{ onClose(self, event): XXX def onClose(self, event): @@ -107,23 +114,17 @@ class MiRCARTGeneralFrame(wx.Frame): panelSkin = wx.Panel(self, wx.ID_ANY) # Initialise menu bar, menus & menu items - self.menuItemsById = {}; menuBar = wx.MenuBar(); - self._initMenus(menuBar, \ - [self.MID_FILE, self.MID_EDIT, self.MID_TOOLS], + # Initialise toolbar & toolbar items + menuBar = self._initMenus(self.LID_MENUS[2], \ self.onFrameCommand) self.SetMenuBar(menuBar) - - # Initialise toolbar & toolbar items - self.toolBarItemsById = {} - self.toolBar = wx.ToolBar(panelSkin, -1, \ - style=wx.HORIZONTAL|wx.TB_FLAT|wx.TB_NODIVIDER) - self.toolBar.SetToolBitmapSize((16,16)) - self._initToolBars(self.toolBar, [self.BID_TOOLBAR], self.onFrameCommand) - self.toolBar.Realize(); self.toolBar.Fit(); + toolBar = self._initToolBars(self.LID_TOOLBARS[2], \ + self.onFrameCommand, panelSkin) # Initialise accelerators (hotkeys) - accelTable = wx.AcceleratorTable( \ - self._initAccelTable(self.AID_EDIT, self.onFrameCommand)) + accelTable = wx.AcceleratorTable( \ + self._initAccelTable(self.LID_ACCELS[2], \ + self.onFrameCommand)) self.SetAcceleratorTable(accelTable) # Initialise status bar From 1099341b5dca581eb4aa80cb0ed876458b6a02d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Mon, 8 Jan 2018 02:45:03 +0100 Subject: [PATCH 083/148] MiRCARTCanvas{,Journal}.py: fix {un,re}do regarding brush sizes >(1,1). --- MiRCARTCanvas.py | 10 +++++++--- MiRCARTCanvasJournal.py | 39 ++++++++++++++++++++++++--------------- 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/MiRCARTCanvas.py b/MiRCARTCanvas.py index 0503889..966136b 100644 --- a/MiRCARTCanvas.py +++ b/MiRCARTCanvas.py @@ -102,7 +102,6 @@ class MiRCARTCanvas(wx.Panel): patchOld[1:] = self._getMapCell(patchOld[0]) self._setMapCell(absMapPoint, *patch[1:]) self._drawPatch(patch, eventDc, tmpDc, atPoint) - self.parentFrame.onCanvasUpdate() if isInherit: return patchOld # }}} @@ -118,6 +117,7 @@ class MiRCARTCanvas(wx.Panel): event, mapPoint, self.brushColours, self.brushSize, \ event.Dragging(), event.LeftIsDown(), event.RightIsDown()) self.canvasJournal.merge(mapPatches, eventDc, tmpDc, mapPoint) + self.parentFrame.onCanvasUpdate() self.parentFrame.onCanvasMotion(event, mapPoint) # }}} # {{{ onMouseWindowEvent(self, event): XXX @@ -168,7 +168,9 @@ class MiRCARTCanvas(wx.Panel): # }}} # {{{ redo(self): XXX def redo(self): - return self.canvasJournal.redo() + result = self.canvasJournal.redo() + self.parentFrame.onCanvasUpdate() + return result # }}} # {{{ resize(self, newCanvasSize): XXX def resize(self, newCanvasSize): @@ -191,7 +193,9 @@ class MiRCARTCanvas(wx.Panel): # }}} # {{{ undo(self): XXX def undo(self): - return self.canvasJournal.undo() + result = self.canvasJournal.undo() + self.parentFrame.onCanvasUpdate() + return result # }}} # {{{ __del__(self): destructor method diff --git a/MiRCARTCanvasJournal.py b/MiRCARTCanvasJournal.py index de9511d..5bdfbbd 100644 --- a/MiRCARTCanvasJournal.py +++ b/MiRCARTCanvasJournal.py @@ -35,18 +35,23 @@ class MiRCARTCanvasJournal(): patch[0], patch, eventDc, tmpDc, (0, 0), True) self.patchesTmp = [] # }}} - # {{{ _pushTmp(self, atPoint, patch): XXX - def _pushTmp(self, absMapPoint): - self.patchesTmp.append([absMapPoint, None, None, None]) + # {{{ _pushTmp(self, atPoint): XXX + def _pushTmp(self, atPoint): + self.patchesTmp.append([atPoint, None, None, None]) # }}} - # {{{ _pushUndo(self, atPoint, patchUndo, patchRedo): XXX - def _pushUndo(self, atPoint, patchUndo, patchRedo): + # {{{ _pushUndo(self, atPoint, patches): XXX + def _pushUndo(self, atPoint, patches): if self.patchesUndoLevel > 0: del self.patchesUndo[0:self.patchesUndoLevel] self.patchesUndoLevel = 0 - absMapPoint = self._relMapPointToAbsMapPoint(patchUndo[0], atPoint) - self.patchesUndo.insert(0, [ \ - [absMapPoint, *patchUndo[1:]], [absMapPoint, *patchRedo[1:]]]) + patchesUndo = [] + for patch in patches: + absMapPoint = self._relMapPointToAbsMapPoint(patch[0][0], atPoint) + patchesUndo.append([ \ + [absMapPoint, *patch[0][1:]], \ + [absMapPoint, *patch[1][1:]]]) + if len(patchesUndo) > 0: + self.patchesUndo.insert(0, patchesUndo) # }}} # {{{ _relMapPointToAbsMapPoint(self, relMapPoint, atPoint): XXX def _relMapPointToAbsMapPoint(self, relMapPoint, atPoint): @@ -54,6 +59,7 @@ class MiRCARTCanvasJournal(): # }}} # {{{ merge(self, mapPatches, eventDc, tmpDc, atPoint): XXX def merge(self, mapPatches, eventDc, tmpDc, atPoint): + patchesUndo = [] for mapPatch in mapPatches: mapPatchTmp = mapPatch[0] if mapPatchTmp: @@ -71,15 +77,17 @@ class MiRCARTCanvasJournal(): patchUndo = \ self.parentCanvas.onJournalUpdate(mapPatchTmp, \ absMapPoint, patch, eventDc, tmpDc, atPoint, True) - self._pushUndo(atPoint, patchUndo, patch) + patchesUndo.append([patchUndo, patch]) + if len(patchesUndo) > 0: + self._pushUndo(atPoint, patchesUndo) # }}} # {{{ redo(self): XXX def redo(self): if self.patchesUndoLevel > 0: self.patchesUndoLevel -= 1 - redoPatch = self.patchesUndo[self.patchesUndoLevel][1] - self.parentCanvas.onJournalUpdate(False, \ - redoPatch[0], redoPatch, None, None, (0, 0)) + for patch in self.patchesUndo[self.patchesUndoLevel]: + self.parentCanvas.onJournalUpdate(False, \ + patch[1][0], patch[1], None, None, (0, 0)) return True else: return False @@ -97,10 +105,11 @@ class MiRCARTCanvasJournal(): # {{{ undo(self): XXX def undo(self): if self.patchesUndo[self.patchesUndoLevel] != None: - undoPatch = self.patchesUndo[self.patchesUndoLevel][0] + patches = self.patchesUndo[self.patchesUndoLevel] self.patchesUndoLevel += 1 - self.parentCanvas.onJournalUpdate(False, \ - undoPatch[0], undoPatch, None, None, (0, 0)) + for patch in patches: + self.parentCanvas.onJournalUpdate(False, \ + patch[0][0], patch[0], None, None, (0, 0)) return True else: return False From 9b5c9be88bd2280b8c4fe4ce87ec0d31a77fc997 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Mon, 8 Jan 2018 02:56:08 +0100 Subject: [PATCH 084/148] MiRCARTFrame.py: adds -<{+,-}> {in,de}crease brush size accelerators (hotkeys). --- MiRCARTFrame.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/MiRCARTFrame.py b/MiRCARTFrame.py index 8f7dcd4..28bc971 100644 --- a/MiRCARTFrame.py +++ b/MiRCARTFrame.py @@ -54,9 +54,9 @@ class MiRCARTFrame(MiRCARTGeneralFrame): CID_PASTE = (0x10c, TID_COMMAND, "Paste", "&Paste", wx.ART_PASTE, None, False) CID_DELETE = (0x10d, TID_COMMAND, "Delete", "De&lete", wx.ART_DELETE, None, False) CID_INCRBRUSH = (0x10e, TID_COMMAND, "Increase brush size", \ - "&Increase brush size", wx.ART_PLUS, None) + "&Increase brush size", wx.ART_PLUS, (wx.ACCEL_CTRL, ord("+"))) CID_DECRBRUSH = (0x10f, TID_COMMAND, "Decrease brush size", \ - "&Decrease brush size", wx.ART_MINUS, None) + "&Decrease brush size", wx.ART_MINUS, (wx.ACCEL_CTRL, ord("-"))) CID_SOLID_BRUSH = (0x110, TID_SELECT, "Solid brush", "&Solid brush", None, None, True) CID_RECT = (0x111, TID_SELECT, "Rectangle", "&Rectangle", None, None, True) CID_CIRCLE = (0x112, TID_SELECT, "Circle", "&Circle", None, None, False) @@ -108,7 +108,7 @@ class MiRCARTFrame(MiRCARTGeneralFrame): # }}} # {{{ Accelerators (hotkeys) AID_EDIT = (0x500, TID_ACCELS, ( \ - CID_NEW, CID_OPEN, CID_SAVE, CID_UNDO, CID_REDO)) + CID_NEW, CID_OPEN, CID_SAVE, CID_UNDO, CID_REDO, CID_INCRBRUSH, CID_DECRBRUSH)) # }}} # {{{ Lists LID_ACCELS = (0x600, TID_LIST, (AID_EDIT)) From 195ddd9af8fdb1f5307f3470677c352abb78b4ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Mon, 8 Jan 2018 16:12:11 +0100 Subject: [PATCH 085/148] MiRCART{Canvas,Frame}.py: adds circle & line tools. MiRCARTTool{Circle,Line}.py: initial implementation. --- MiRCART.py | 3 +- MiRCARTCanvas.py | 21 +++++---- MiRCARTCanvasJournal.py | 28 +++++------ MiRCARTFrame.py | 25 ++++++---- MiRCARTToolCircle.py | 56 ++++++++++++++++++++++ MiRCARTToolLine.py | 100 ++++++++++++++++++++++++++++++++++++++++ MiRCARTToolRect.py | 4 +- 7 files changed, 199 insertions(+), 38 deletions(-) create mode 100644 MiRCARTToolCircle.py create mode 100644 MiRCARTToolLine.py diff --git a/MiRCART.py b/MiRCART.py index 28e9d36..c03e793 100755 --- a/MiRCART.py +++ b/MiRCART.py @@ -23,14 +23,13 @@ # from MiRCARTFrame import MiRCARTFrame -from MiRCARTToolRect import MiRCARTToolRect import sys, wx # # Entry point def main(*argv): wxApp = wx.App(False) - MiRCARTFrame(None, canvasTools=[MiRCARTToolRect]) + MiRCARTFrame(None) wxApp.MainLoop() if __name__ == "__main__": main(*sys.argv) diff --git a/MiRCARTCanvas.py b/MiRCARTCanvas.py index 966136b..eb341dd 100644 --- a/MiRCARTCanvas.py +++ b/MiRCARTCanvas.py @@ -31,10 +31,11 @@ class MiRCARTCanvas(wx.Panel): """XXX""" parentFrame = None canvasPos = canvasSize = canvasWinSize = cellSize = None - canvasBitmap = canvasMap = canvasTools = None + canvasBitmap = canvasMap = None brushColours = brushPos = brushSize = None mircBrushes = mircPens = None canvasJournal = canvasStore = None + canvasCurTool = None # {{{ _initBrushesAndPens(self): XXX def _initBrushesAndPens(self): @@ -112,12 +113,12 @@ class MiRCARTCanvas(wx.Panel): tmpDc.SelectObject(self.canvasBitmap) eventPoint = event.GetLogicalPosition(eventDc) mapPoint = self._eventPointToMapPoint(eventPoint) - for tool in self.canvasTools: - mapPatches = tool.onMouseEvent( \ - event, mapPoint, self.brushColours, self.brushSize, \ - event.Dragging(), event.LeftIsDown(), event.RightIsDown()) - self.canvasJournal.merge(mapPatches, eventDc, tmpDc, mapPoint) - self.parentFrame.onCanvasUpdate() + tool = self.canvasCurTool + mapPatches = tool.onMouseEvent( \ + event, mapPoint, self.brushColours, self.brushSize, \ + event.Dragging(), event.LeftIsDown(), event.RightIsDown()) + self.canvasJournal.merge(mapPatches, eventDc, tmpDc, mapPoint) + self.parentFrame.onCanvasUpdate() self.parentFrame.onCanvasMotion(event, mapPoint) # }}} # {{{ onMouseWindowEvent(self, event): XXX @@ -211,17 +212,17 @@ class MiRCARTCanvas(wx.Panel): # }}} # - # _init__(self, parent, parentFrame, canvasPos, cellSize, canvasSize, canvasTools): initialisation method - def __init__(self, parent, parentFrame, canvasPos, canvasSize, canvasTools, cellSize): + # _init__(self, parent, parentFrame, canvasPos, canvasSize, cellSize): initialisation method + def __init__(self, parent, parentFrame, canvasPos, canvasSize, cellSize): self.parentFrame = parentFrame self.canvasPos = canvasPos; self.canvasSize = canvasSize; - self.canvasTools = [canvasTool(self) for canvasTool in canvasTools] self.cellSize = cellSize self.brushColours = [4, 1]; self._initBrushesAndPens(); self.brushPos = [0, 0]; self.brushSize = [1, 1]; self.canvasJournal = MiRCARTCanvasJournal(parentCanvas=self) self.canvasStore = MiRCARTCanvasStore(parentCanvas=self) + self.canvasCurTool = None super().__init__(parent, pos=canvasPos, \ size=[w*h for w,h in zip(canvasSize, cellSize)]) diff --git a/MiRCARTCanvasJournal.py b/MiRCARTCanvasJournal.py index 5bdfbbd..9ac34d2 100644 --- a/MiRCARTCanvasJournal.py +++ b/MiRCARTCanvasJournal.py @@ -46,17 +46,12 @@ class MiRCARTCanvasJournal(): self.patchesUndoLevel = 0 patchesUndo = [] for patch in patches: - absMapPoint = self._relMapPointToAbsMapPoint(patch[0][0], atPoint) - patchesUndo.append([ \ - [absMapPoint, *patch[0][1:]], \ - [absMapPoint, *patch[1][1:]]]) + patchesUndo.append([ \ + [patch[0], *patch[0][1:]], \ + [patch[0], *patch[1][1:]]]) if len(patchesUndo) > 0: self.patchesUndo.insert(0, patchesUndo) # }}} - # {{{ _relMapPointToAbsMapPoint(self, relMapPoint, atPoint): XXX - def _relMapPointToAbsMapPoint(self, relMapPoint, atPoint): - return [a+b for a,b in zip(atPoint, relMapPoint)] - # }}} # {{{ merge(self, mapPatches, eventDc, tmpDc, atPoint): XXX def merge(self, mapPatches, eventDc, tmpDc, atPoint): patchesUndo = [] @@ -65,18 +60,17 @@ class MiRCARTCanvasJournal(): if mapPatchTmp: self._popTmp(eventDc, tmpDc) for patch in mapPatch[1]: - absMapPoint = self._relMapPointToAbsMapPoint(patch[0], atPoint) - if absMapPoint[0] >= self.parentCanvas.canvasSize[0] \ - or absMapPoint[1] >= self.parentCanvas.canvasSize[1]: + if patch[0][0] >= self.parentCanvas.canvasSize[0] \ + or patch[0][1] >= self.parentCanvas.canvasSize[1]: continue elif mapPatchTmp: - self._pushTmp(absMapPoint) - self.parentCanvas.onJournalUpdate(mapPatchTmp, \ - absMapPoint, patch, eventDc, tmpDc, atPoint) + self._pushTmp(patch[0]) + self.parentCanvas.onJournalUpdate(mapPatchTmp, \ + patch[0], patch, eventDc, tmpDc, (0, 0)) else: - patchUndo = \ - self.parentCanvas.onJournalUpdate(mapPatchTmp, \ - absMapPoint, patch, eventDc, tmpDc, atPoint, True) + patchUndo = \ + self.parentCanvas.onJournalUpdate(mapPatchTmp, \ + patch[0], patch, eventDc, tmpDc, (0, 0), True) patchesUndo.append([patchUndo, patch]) if len(patchesUndo) > 0: self._pushUndo(atPoint, patchesUndo) diff --git a/MiRCARTFrame.py b/MiRCARTFrame.py index 28bc971..56e7b1d 100644 --- a/MiRCARTFrame.py +++ b/MiRCARTFrame.py @@ -26,13 +26,16 @@ from MiRCARTCanvas import MiRCARTCanvas, haveUrllib from MiRCARTColours import MiRCARTColours from MiRCARTGeneralFrame import MiRCARTGeneralFrame, \ TID_ACCELS, TID_COMMAND, TID_LIST, TID_MENU, TID_NOTHING, TID_SELECT, TID_TOOLBAR +from MiRCARTToolCircle import MiRCARTToolCircle +from MiRCARTToolLine import MiRCARTToolLine +from MiRCARTToolRect import MiRCARTToolRect import os, wx class MiRCARTFrame(MiRCARTGeneralFrame): """XXX""" panelCanvas = canvasPathName = None - canvasPos = canvasSize = canvasTools = cellSize = None + canvasPos = canvasSize = cellSize = None # {{{ Commands # Id Type Id Labels Icon bitmap Accelerator [Initial state] @@ -355,11 +358,17 @@ class MiRCARTFrame(MiRCARTGeneralFrame): elif cid == self.CID_SOLID_BRUSH[0]: pass elif cid == self.CID_RECT[0]: - pass + self.menuItemsById[cid].Check(True) + self.panelCanvas.canvasCurTool = \ + MiRCARTToolRect(self.panelCanvas) elif cid == self.CID_CIRCLE[0]: - pass + self.menuItemsById[cid].Check(True) + self.panelCanvas.canvasCurTool = \ + MiRCARTToolCircle(self.panelCanvas) elif cid == self.CID_LINE[0]: - pass + self.menuItemsById[cid].Check(True) + self.panelCanvas.canvasCurTool = \ + MiRCARTToolLine(self.panelCanvas) elif cid >= self.CID_COLOUR00[0] \ and cid <= self.CID_COLOUR15[0]: numColour = cid - self.CID_COLOUR00[0] @@ -376,16 +385,16 @@ class MiRCARTFrame(MiRCARTGeneralFrame): # }}} # - # __init__(self, parent, appSize=(800, 600), canvasPos=(25, 50), cellSize=(7, 14), canvasSize=(100, 30), canvasTools=[]): initialisation method - def __init__(self, parent, appSize=(800, 600), canvasPos=(25, 50), cellSize=(7, 14), canvasSize=(100, 30), canvasTools=[]): + # __init__(self, parent, appSize=(800, 600), canvasPos=(25, 50), canvasSize=(100, 30), cellSize=(7, 14)): initialisation method + def __init__(self, parent, appSize=(800, 600), canvasPos=(25, 50), canvasSize=(100, 30), cellSize=(7, 14)): panelSkin = super().__init__(parent, wx.ID_ANY, "MiRCART", size=appSize) self._setPaletteToolBitmaps() self.canvasPos = canvasPos; self.cellSize = cellSize; self.canvasSize = canvasSize; self.canvasPathName = None - self.canvasTools = canvasTools self.panelCanvas = MiRCARTCanvas(panelSkin, parentFrame=self, \ canvasPos=self.canvasPos, canvasSize=self.canvasSize, \ - canvasTools=self.canvasTools, cellSize=self.cellSize) + cellSize=self.cellSize) + self.panelCanvas.canvasCurTool = MiRCARTToolRect(self.panelCanvas) self.canvasNew() # vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/MiRCARTToolCircle.py b/MiRCARTToolCircle.py new file mode 100644 index 0000000..8218e85 --- /dev/null +++ b/MiRCARTToolCircle.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +# +# MiRCARTToolCircle.py -- XXX +# Copyright (c) 2018 Lucio Andrés Illanes Albornoz +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +from MiRCARTTool import MiRCARTTool + +class MiRCARTToolCircle(MiRCARTTool): + """XXX""" + + # + # onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown): XXX + def onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown): + brushColours = brushColours.copy() + if isLeftDown: + brushColours[1] = brushColours[0] + elif isRightDown: + brushColours[0] = brushColours[1] + else: + brushColours[1] = brushColours[0] + brushPatches = [] + _brushSize = brushSize[0]*2 + originPoint = (_brushSize/2, _brushSize/2) + radius = _brushSize + for brushY in range(-radius, radius + 1): + for brushX in range(-radius, radius + 1): + if ((brushX**2)+(brushY**2) < (((radius**2)+radius)*0.8)): + brushPatches.append([ \ + [atPoint[0] + int(originPoint[0]+brushX), \ + atPoint[1] + int(originPoint[1]+brushY)], \ + brushColours, 0, " "]) + if isLeftDown or isRightDown: + return [[False, brushPatches], [True, brushPatches]] + else: + return [[True, brushPatches]] + +# vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/MiRCARTToolLine.py b/MiRCARTToolLine.py new file mode 100644 index 0000000..18652d3 --- /dev/null +++ b/MiRCARTToolLine.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +# +# MiRCARTToolLine.py -- XXX +# Copyright (c) 2018 Lucio Andrés Illanes Albornoz +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +from MiRCARTTool import MiRCARTTool + +class MiRCARTToolLine(MiRCARTTool): + """XXX""" + toolOriginPoint = toolState = None + + TS_NONE = 0 + TS_ORIGIN = 1 + + # {{{ _pointDelta(self, a, b): XXX + def _pointDelta(self, a, b): + return [a2-a1 for a1, a2 in zip(a, b)] + # }}} + # {{{ _pointSwap(self, a, b): XXX + def _pointSwap(self, a, b): + return [b, a] + # }}} + # {{{ _getLine(self, brushColours, originPoint, targetPoint): XXX + def _getLine(self, brushColours, originPoint, targetPoint): + originPoint = originPoint.copy(); targetPoint = targetPoint.copy(); + pointDelta = self._pointDelta(originPoint, targetPoint) + lineXSign = 1 if pointDelta[0] > 0 else -1; + lineYSign = 1 if pointDelta[1] > 0 else -1; + pointDelta = [abs(a) for a in pointDelta] + if pointDelta[0] > pointDelta[1]: + lineXX, lineXY, lineYX, lineYY = lineXSign, 0, 0, lineYSign + else: + pointDelta = [pointDelta[1], pointDelta[0]] + lineXX, lineXY, lineYX, lineYY = 0, lineYSign, lineXSign, 0 + lineD = 2 * pointDelta[1] - pointDelta[0]; lineY = 0; + linePatches = [] + for lineX in range(pointDelta[0] + 1): + linePatches.append([[ \ + originPoint[0] + lineX*lineXX + lineY*lineYX, \ + originPoint[1] + lineX*lineXY + lineY*lineYY], \ + brushColours, 0, " "]) + if lineD > 0: + lineD -= pointDelta[0]; lineY += 1; + lineD += pointDelta[1] + return linePatches + # }}} + + # + # onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown): XXX + def onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown): + brushColours = brushColours.copy() + if isLeftDown: + brushColours[1] = brushColours[0] + elif isRightDown: + brushColours[0] = brushColours[1] + else: + brushColours[1] = brushColours[0] + brushPatches = []; tmpPatches = []; + if self.toolState == self.TS_NONE: + if isLeftDown or isRightDown: + self.toolOriginPoint = list(atPoint) + self.toolState = self.TS_ORIGIN + tmpPatches.append([atPoint, brushColours, 0, " "]) + return [[True, tmpPatches]] + elif self.toolState == self.TS_ORIGIN: + targetPoint = list(atPoint) + originPoint = self.toolOriginPoint + brushPatches = self._getLine(brushColours, originPoint, targetPoint) + if isLeftDown or isRightDown: + self.toolState = self.TS_NONE + return [[False, brushPatches], [True, brushPatches]] + else: + return [[True, brushPatches]] + + # __init__(self, *args): initialisation method + def __init__(self, *args): + super().__init__(*args) + self.toolOriginPoint = None + self.toolState = self.TS_NONE + +# vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/MiRCARTToolRect.py b/MiRCARTToolRect.py index 66a8864..d2452c5 100644 --- a/MiRCARTToolRect.py +++ b/MiRCARTToolRect.py @@ -40,7 +40,9 @@ class MiRCARTToolRect(MiRCARTTool): brushPatches = [] for brushRow in range(brushSize[1]): for brushCol in range(brushSize[0] * 2): - brushPatches.append([[brushCol, brushRow], \ + brushPatches.append([[ \ + atPoint[0] + brushCol, \ + atPoint[1] + brushRow], \ brushColours, 0, " "]) if isLeftDown or isRightDown: return [[False, brushPatches], [True, brushPatches]] From f431adb3810b7a9a3347bf676d1e385ac78bc84f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Mon, 8 Jan 2018 17:47:58 +0100 Subject: [PATCH 086/148] MiRCART.png: updates screenshot. --- MiRCART.png | Bin 30019 -> 35691 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/MiRCART.png b/MiRCART.png index a803fefefcb79a42a9910309d7d87cb6e6205357..26cfcdf0a23fb17135c6c8066bd1bb9543941e20 100644 GIT binary patch literal 35691 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 literal 30019 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 From 2a7f281d29a81eadc1f953ec3519fb4f84f149a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Mon, 8 Jan 2018 20:28:43 +0100 Subject: [PATCH 087/148] MiRCART{General,}Frame.py, assets/*.png: adds icons for tools. MiRCART{Frame,ToolText}.py: initial implementation of Text tool. --- MiRCARTFrame.py | 123 ++++++++++++++++++++++------------------- MiRCARTGeneralFrame.py | 37 +++++++++---- MiRCARTToolText.py | 52 +++++++++++++++++ assets/toolCircle.png | Bin 0 -> 360 bytes assets/toolLine.png | Bin 0 -> 371 bytes assets/toolRect.png | Bin 0 -> 220 bytes assets/toolText.png | Bin 0 -> 306 bytes 7 files changed, 144 insertions(+), 68 deletions(-) create mode 100644 MiRCARTToolText.py create mode 100644 assets/toolCircle.png create mode 100644 assets/toolLine.png create mode 100644 assets/toolRect.png create mode 100644 assets/toolText.png diff --git a/MiRCARTFrame.py b/MiRCARTFrame.py index 56e7b1d..1e14a7d 100644 --- a/MiRCARTFrame.py +++ b/MiRCARTFrame.py @@ -29,6 +29,7 @@ from MiRCARTGeneralFrame import MiRCARTGeneralFrame, \ from MiRCARTToolCircle import MiRCARTToolCircle from MiRCARTToolLine import MiRCARTToolLine from MiRCARTToolRect import MiRCARTToolRect +from MiRCARTToolText import MiRCARTToolText import os, wx @@ -38,48 +39,51 @@ class MiRCARTFrame(MiRCARTGeneralFrame): canvasPos = canvasSize = cellSize = None # {{{ Commands - # Id Type Id Labels Icon bitmap Accelerator [Initial state] - CID_NEW = (0x100, TID_COMMAND, "New", "&New", wx.ART_NEW, (wx.ACCEL_CTRL, ord("N"))) - CID_OPEN = (0x101, TID_COMMAND, "Open", "&Open", wx.ART_FILE_OPEN, (wx.ACCEL_CTRL, ord("O"))) - CID_SAVE = (0x102, TID_COMMAND, "Save", "&Save", wx.ART_FILE_SAVE, (wx.ACCEL_CTRL, ord("S"))) - CID_SAVEAS = (0x103, TID_COMMAND, "Save As...", "Save &As...", wx.ART_FILE_SAVE_AS, None) - CID_EXPORT_AS_PNG = (0x104, TID_COMMAND, "Export as PNG...", \ - "Export as PN&G...", None, None) - CID_EXPORT_IMGUR = (0x105, TID_COMMAND, "Export to Imgur...", \ - "Export to I&mgur...", None, None, haveUrllib) - CID_EXPORT_PASTEBIN = (0x106, TID_COMMAND, "Export to Pastebin...", \ - "Export to Pasteb&in...", None, None, haveUrllib) - CID_EXIT = (0x107, TID_COMMAND, "Exit", "E&xit", None, None) - CID_UNDO = (0x108, TID_COMMAND, "Undo", "&Undo", wx.ART_UNDO, (wx.ACCEL_CTRL, ord("Z")), False) - CID_REDO = (0x109, TID_COMMAND, "Redo", "&Redo", wx.ART_REDO, (wx.ACCEL_CTRL, ord("Y")), False) - CID_CUT = (0x10a, TID_COMMAND, "Cut", "Cu&t", wx.ART_CUT, None, False) - CID_COPY = (0x10b, TID_COMMAND, "Copy", "&Copy", wx.ART_COPY, None, False) - CID_PASTE = (0x10c, TID_COMMAND, "Paste", "&Paste", wx.ART_PASTE, None, False) - CID_DELETE = (0x10d, TID_COMMAND, "Delete", "De&lete", wx.ART_DELETE, None, False) - CID_INCRBRUSH = (0x10e, TID_COMMAND, "Increase brush size", \ - "&Increase brush size", wx.ART_PLUS, (wx.ACCEL_CTRL, ord("+"))) - CID_DECRBRUSH = (0x10f, TID_COMMAND, "Decrease brush size", \ - "&Decrease brush size", wx.ART_MINUS, (wx.ACCEL_CTRL, ord("-"))) - CID_SOLID_BRUSH = (0x110, TID_SELECT, "Solid brush", "&Solid brush", None, None, True) - CID_RECT = (0x111, TID_SELECT, "Rectangle", "&Rectangle", None, None, True) - CID_CIRCLE = (0x112, TID_SELECT, "Circle", "&Circle", None, None, False) - CID_LINE = (0x113, TID_SELECT, "Line", "&Line", None, None, False) - CID_COLOUR00 = (0x114, TID_COMMAND, "Colour #00", "Colour #00", None, None) - CID_COLOUR01 = (0x115, TID_COMMAND, "Colour #01", "Colour #01", None, None) - CID_COLOUR02 = (0x116, TID_COMMAND, "Colour #02", "Colour #02", None, None) - CID_COLOUR03 = (0x117, TID_COMMAND, "Colour #03", "Colour #03", None, None) - CID_COLOUR04 = (0x118, TID_COMMAND, "Colour #04", "Colour #04", None, None) - CID_COLOUR05 = (0x119, TID_COMMAND, "Colour #05", "Colour #05", None, None) - CID_COLOUR06 = (0x11a, TID_COMMAND, "Colour #06", "Colour #06", None, None) - CID_COLOUR07 = (0x11b, TID_COMMAND, "Colour #07", "Colour #07", None, None) - CID_COLOUR08 = (0x11c, TID_COMMAND, "Colour #08", "Colour #08", None, None) - CID_COLOUR09 = (0x11d, TID_COMMAND, "Colour #09", "Colour #09", None, None) - CID_COLOUR10 = (0x11e, TID_COMMAND, "Colour #10", "Colour #10", None, None) - CID_COLOUR11 = (0x11f, TID_COMMAND, "Colour #11", "Colour #11", None, None) - CID_COLOUR12 = (0x120, TID_COMMAND, "Colour #12", "Colour #12", None, None) - CID_COLOUR13 = (0x121, TID_COMMAND, "Colour #13", "Colour #13", None, None) - CID_COLOUR14 = (0x122, TID_COMMAND, "Colour #14", "Colour #14", None, None) - CID_COLOUR15 = (0x123, TID_COMMAND, "Colour #15", "Colour #15", None, None) + # Id Type Id Labels Icon bitmap Accelerator [Initial state] + CID_NEW = [0x100, TID_COMMAND, "New", "&New", ["", wx.ART_NEW], [wx.ACCEL_CTRL, ord("N")]] + CID_OPEN = [0x101, TID_COMMAND, "Open", "&Open", ["", wx.ART_FILE_OPEN], [wx.ACCEL_CTRL, ord("O")]] + CID_SAVE = [0x102, TID_COMMAND, "Save", "&Save", ["", wx.ART_FILE_SAVE], [wx.ACCEL_CTRL, ord("S")]] + CID_SAVEAS = [0x103, TID_COMMAND, "Save As...", "Save &As...", ["", wx.ART_FILE_SAVE_AS], None] + CID_EXPORT_AS_PNG = [0x104, TID_COMMAND, "Export as PNG...", \ + "Export as PN&G...", None, None] + CID_EXPORT_IMGUR = [0x105, TID_COMMAND, "Export to Imgur...", \ + "Export to I&mgur...", None, None, haveUrllib] + CID_EXPORT_PASTEBIN = [0x106, TID_COMMAND, "Export to Pastebin...", \ + "Export to Pasteb&in...", None, None, haveUrllib] + CID_EXIT = [0x107, TID_COMMAND, "Exit", "E&xit", None, None] + CID_UNDO = [0x108, TID_COMMAND, "Undo", "&Undo", ["", wx.ART_UNDO], [wx.ACCEL_CTRL, ord("Z")], False] + CID_REDO = [0x109, TID_COMMAND, "Redo", "&Redo", ["", wx.ART_REDO], [wx.ACCEL_CTRL, ord("Y")], False] + CID_CUT = [0x10a, TID_COMMAND, "Cut", "Cu&t", ["", wx.ART_CUT], None, False] + CID_COPY = [0x10b, TID_COMMAND, "Copy", "&Copy", ["", wx.ART_COPY], None, False] + CID_PASTE = [0x10c, TID_COMMAND, "Paste", "&Paste", ["", wx.ART_PASTE], None, False] + CID_DELETE = [0x10d, TID_COMMAND, "Delete", "De&lete", ["", wx.ART_DELETE], None, False] + CID_INCRBRUSH = [0x10e, TID_COMMAND, "Increase brush size", \ + "&Increase brush size", ["", wx.ART_PLUS], [wx.ACCEL_CTRL, ord("+")]] + CID_DECRBRUSH = [0x10f, TID_COMMAND, "Decrease brush size", \ + "&Decrease brush size", ["", wx.ART_MINUS], [wx.ACCEL_CTRL, ord("-")]] + CID_SOLID_BRUSH = [0x110, TID_SELECT, "Solid brush", "&Solid brush", None, None, True] + + CID_RECT = [0x150, TID_SELECT, "Rectangle", "&Rectangle", ["toolRect.png"], [wx.ACCEL_CTRL, ord("R")], True] + CID_CIRCLE = [0x151, TID_SELECT, "Circle", "&Circle", ["toolCircle.png"], [wx.ACCEL_CTRL, ord("C")], False] + CID_LINE = [0x152, TID_SELECT, "Line", "&Line", ["toolLine.png"], [wx.ACCEL_CTRL, ord("L")], False] + CID_TEXT = [0x153, TID_SELECT, "Text", "&Text", ["toolText.png"], [wx.ACCEL_CTRL, ord("T")], False] + + CID_COLOUR00 = [0x1a0, TID_COMMAND, "Colour #00", "Colour #00", None, None] + CID_COLOUR01 = [0x1a1, TID_COMMAND, "Colour #01", "Colour #01", None, None] + CID_COLOUR02 = [0x1a2, TID_COMMAND, "Colour #02", "Colour #02", None, None] + CID_COLOUR03 = [0x1a3, TID_COMMAND, "Colour #03", "Colour #03", None, None] + CID_COLOUR04 = [0x1a4, TID_COMMAND, "Colour #04", "Colour #04", None, None] + CID_COLOUR05 = [0x1a5, TID_COMMAND, "Colour #05", "Colour #05", None, None] + CID_COLOUR06 = [0x1a6, TID_COMMAND, "Colour #06", "Colour #06", None, None] + CID_COLOUR07 = [0x1a7, TID_COMMAND, "Colour #07", "Colour #07", None, None] + CID_COLOUR08 = [0x1a8, TID_COMMAND, "Colour #08", "Colour #08", None, None] + CID_COLOUR09 = [0x1a9, TID_COMMAND, "Colour #09", "Colour #09", None, None] + CID_COLOUR10 = [0x1aa, TID_COMMAND, "Colour #10", "Colour #10", None, None] + CID_COLOUR11 = [0x1ab, TID_COMMAND, "Colour #11", "Colour #11", None, None] + CID_COLOUR12 = [0x1ac, TID_COMMAND, "Colour #12", "Colour #12", None, None] + CID_COLOUR13 = [0x1ad, TID_COMMAND, "Colour #13", "Colour #13", None, None] + CID_COLOUR14 = [0x1ae, TID_COMMAND, "Colour #14", "Colour #14", None, None] + CID_COLOUR15 = [0x1af, TID_COMMAND, "Colour #15", "Colour #15", None, None] # }}} # {{{ Non-items NID_MENU_SEP = (0x200, TID_NOTHING) @@ -95,7 +99,7 @@ class MiRCARTFrame(MiRCARTGeneralFrame): CID_CUT, CID_COPY, CID_PASTE, CID_DELETE, NID_MENU_SEP, \ CID_INCRBRUSH, CID_DECRBRUSH, CID_SOLID_BRUSH)) MID_TOOLS = (0x302, TID_MENU, "Tools", "&Tools", ( \ - CID_RECT, CID_CIRCLE, CID_LINE)) + CID_RECT, CID_CIRCLE, CID_LINE, CID_TEXT)) # }}} # {{{ Toolbars BID_TOOLBAR = (0x400, TID_TOOLBAR, ( \ @@ -103,7 +107,7 @@ class MiRCARTFrame(MiRCARTGeneralFrame): CID_UNDO, CID_REDO, NID_TOOLBAR_SEP, \ CID_CUT, CID_COPY, CID_PASTE, CID_DELETE, NID_TOOLBAR_SEP, \ CID_INCRBRUSH, CID_DECRBRUSH, CID_SOLID_BRUSH, NID_TOOLBAR_SEP, \ - CID_RECT, CID_CIRCLE, CID_LINE, NID_TOOLBAR_SEP, \ + CID_RECT, CID_CIRCLE, CID_LINE, CID_TEXT, NID_TOOLBAR_SEP, \ CID_COLOUR00, CID_COLOUR01, CID_COLOUR02, CID_COLOUR03, CID_COLOUR04, \ CID_COLOUR05, CID_COLOUR06, CID_COLOUR07, CID_COLOUR08, CID_COLOUR09, \ CID_COLOUR10, CID_COLOUR11, CID_COLOUR12, CID_COLOUR13, CID_COLOUR14, \ @@ -119,17 +123,8 @@ class MiRCARTFrame(MiRCARTGeneralFrame): LID_TOOLBARS = (0x602, TID_LIST, (BID_TOOLBAR)) # }}} - # {{{ _dialogSaveChanges(self) - def _dialogSaveChanges(self): - with wx.MessageDialog(self, \ - "Do you want to save changes to {}?".format( \ - self.canvasPathName), "MiRCART", \ - wx.CANCEL|wx.CANCEL_DEFAULT|wx.ICON_QUESTION|wx.YES_NO) as dialog: - dialogChoice = dialog.ShowModal() - return dialogChoice - # }}} - # {{{ _setPaletteToolBitmaps(self): XXX - def _setPaletteToolBitmaps(self): + # {{{ _initPaletteToolBitmaps(self): XXX + def _initPaletteToolBitmaps(self): paletteDescr = ( \ self.CID_COLOUR00, self.CID_COLOUR01, self.CID_COLOUR02, self.CID_COLOUR03, self.CID_COLOUR04, \ self.CID_COLOUR05, self.CID_COLOUR06, self.CID_COLOUR07, self.CID_COLOUR08, self.CID_COLOUR09, \ @@ -145,8 +140,16 @@ class MiRCARTFrame(MiRCARTGeneralFrame): toolBitmapDc.SetBackground(toolBitmapBrush) toolBitmapDc.SetPen(wx.Pen(wx.Colour(toolBitmapColour), 1)) toolBitmapDc.DrawRectangle(0, 0, 16, 16) - self.toolBar.SetToolNormalBitmap( \ - paletteDescr[numColour][0], toolBitmap) + paletteDescr[numColour][4] = ["", None, toolBitmap] + # }}} + # {{{ _dialogSaveChanges(self) + def _dialogSaveChanges(self): + with wx.MessageDialog(self, \ + "Do you want to save changes to {}?".format( \ + self.canvasPathName), "MiRCART", \ + wx.CANCEL|wx.CANCEL_DEFAULT|wx.ICON_QUESTION|wx.YES_NO) as dialog: + dialogChoice = dialog.ShowModal() + return dialogChoice # }}} # {{{ _updateStatusBar(self, showColours=None, showFileName=True, showPos=None): XXX def _updateStatusBar(self, showColours=True, showFileName=True, showPos=True): @@ -369,6 +372,10 @@ class MiRCARTFrame(MiRCARTGeneralFrame): self.menuItemsById[cid].Check(True) self.panelCanvas.canvasCurTool = \ MiRCARTToolLine(self.panelCanvas) + elif cid == self.CID_TEXT[0]: + self.menuItemsById[cid].Check(True) + self.panelCanvas.canvasCurTool = \ + MiRCARTToolText(self.panelCanvas) elif cid >= self.CID_COLOUR00[0] \ and cid <= self.CID_COLOUR15[0]: numColour = cid - self.CID_COLOUR00[0] @@ -387,8 +394,8 @@ class MiRCARTFrame(MiRCARTGeneralFrame): # # __init__(self, parent, appSize=(800, 600), canvasPos=(25, 50), canvasSize=(100, 30), cellSize=(7, 14)): initialisation method def __init__(self, parent, appSize=(800, 600), canvasPos=(25, 50), canvasSize=(100, 30), cellSize=(7, 14)): + self._initPaletteToolBitmaps() panelSkin = super().__init__(parent, wx.ID_ANY, "MiRCART", size=appSize) - self._setPaletteToolBitmaps() self.canvasPos = canvasPos; self.cellSize = cellSize; self.canvasSize = canvasSize; self.canvasPathName = None self.panelCanvas = MiRCARTCanvas(panelSkin, parentFrame=self, \ diff --git a/MiRCARTGeneralFrame.py b/MiRCARTGeneralFrame.py index 0c127e0..7bdf00a 100644 --- a/MiRCARTGeneralFrame.py +++ b/MiRCARTGeneralFrame.py @@ -75,29 +75,45 @@ class MiRCARTGeneralFrame(wx.Frame): # {{{ _initToolBars(self, toolBarsDescr, handler): XXX def _initToolBars(self, toolBarsDescr, handler, panelSkin): self.toolBarItemsById = {} - self.toolBar = wx.ToolBar(panelSkin, -1, \ + self.toolBar = wx.ToolBar(panelSkin, -1, \ style=wx.HORIZONTAL|wx.TB_FLAT|wx.TB_NODIVIDER) self.toolBar.SetToolBitmapSize((16,16)) for toolBarItem in toolBarsDescr[2]: if toolBarItem == self.NID_TOOLBAR_SEP: self.toolBar.AddSeparator() else: - if toolBarItem[4] != None: - toolBarItemIcon = wx.ArtProvider.GetBitmap( \ - toolBarItem[4], wx.ART_TOOLBAR, (16,16)) - else: - toolBarItemIcon = wx.ArtProvider.GetBitmap( \ - wx.ART_HELP, wx.ART_TOOLBAR, (16,16)) - toolBarItemWindow = self.toolBar.AddTool( \ - toolBarItem[0], toolBarItem[2], toolBarItemIcon) + toolBarItemWindow = self.toolBar.AddTool( \ + toolBarItem[0], toolBarItem[2], toolBarItem[4][2]) self.toolBarItemsById[toolBarItem[0]] = toolBarItemWindow - if len(toolBarItem) == 7 \ + if len(toolBarItem) == 7 \ and toolBarItem[1] == TID_COMMAND: toolBarItemWindow.Enable(toolBarItem[6]) self.Bind(wx.EVT_TOOL, handler, toolBarItemWindow) self.Bind(wx.EVT_TOOL_RCLICKED, handler, toolBarItemWindow) self.toolBar.Realize(); self.toolBar.Fit(); # }}} + # {{{ _initToolBitmaps(self): XXX + def _initToolBitmaps(self, toolBarsDescr): + for toolBarItem in toolBarsDescr[2]: + if toolBarItem == self.NID_TOOLBAR_SEP: + continue + elif toolBarItem[4] == None: + toolBarItem[4] = ["", None, wx.ArtProvider.GetBitmap( \ + wx.ART_HELP, wx.ART_TOOLBAR, (16,16))] + elif toolBarItem[4][0] == "" \ + and toolBarItem[4][1] != None: + toolBarItem[4] = ["", None, wx.ArtProvider.GetBitmap( \ + toolBarItem[4][1], wx.ART_TOOLBAR, (16,16))] + elif toolBarItem[4][0] == "" \ + and toolBarItem[4][1] == None: + toolBarItem[4] = ["", None, toolBarItem[4][2]] + elif toolBarItem[4][0] != "": + toolBitmap = wx.Bitmap((16,16)) + toolBitmap.LoadFile( \ + os.path.join("assets", toolBarItem[4][0]), \ + wx.BITMAP_TYPE_ANY) + toolBarItem[4] = ["", None, toolBitmap] + # }}} # {{{ onClose(self, event): XXX def onClose(self, event): self.Destroy(); self.__del__(); @@ -118,6 +134,7 @@ class MiRCARTGeneralFrame(wx.Frame): menuBar = self._initMenus(self.LID_MENUS[2], \ self.onFrameCommand) self.SetMenuBar(menuBar) + self._initToolBitmaps(self.LID_TOOLBARS[2]) toolBar = self._initToolBars(self.LID_TOOLBARS[2], \ self.onFrameCommand, panelSkin) diff --git a/MiRCARTToolText.py b/MiRCARTToolText.py new file mode 100644 index 0000000..9fd18ab --- /dev/null +++ b/MiRCARTToolText.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# +# MiRCARTToolText.py -- XXX +# Copyright (c) 2018 Lucio Andrés Illanes Albornoz +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +from MiRCARTTool import MiRCARTTool + +class MiRCARTToolText(MiRCARTTool): + """XXX""" + + # + # onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown): XXX + def onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown): + brushColours = brushColours.copy() + if isLeftDown: + brushColours[1] = brushColours[0] + elif isRightDown: + brushColours[0] = brushColours[1] + else: + brushColours[1] = brushColours[0] + brushPatches = [] + for brushRow in range(brushSize[1]): + for brushCol in range(brushSize[0] * 2): + brushPatches.append([[ \ + atPoint[0] + brushCol, \ + atPoint[1] + brushRow], \ + brushColours, 0, " "]) + if isLeftDown or isRightDown: + return [[False, brushPatches], [True, brushPatches]] + else: + return [[True, brushPatches]] + +# vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/assets/toolCircle.png b/assets/toolCircle.png new file mode 100644 index 0000000000000000000000000000000000000000..9e34f7288272bb8dda0b54fe26d13a6d1998b2a2 GIT binary patch literal 360 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qucLCF%=h?3y^w370~qEv>0#LT=By}Z;C1rt33 zJwwYY*EWF^-SBjA46*QEI!RIJkb!_}xPrETsHn{9MNE;64qwtby0}=Hk`)9_$SM6f zpEBLoBmKn|<9xN><^S_8xj1pm*67OC=vI>5(GcG9dSAqB7A4*tOl+!o3p{N+KbRJ4 zl?OI6Te!Wwtn$&qm^W)(ZQz;r2ZaqRX1?R9@YA!pS~R2EbaAHM%IH5c*FAV#xMPNI z@yWBiUpUrgS^oIKF*#H3=5dw!kfpm$hd*GH;8~XLx+gvN64PEczp&4;Z>C9oP|Tbx yWTJ1cBJ6rDfu%TwiFw1xu-V+%nqB{epYWbJq^No$J@+`!j|`r!elF{r5}E+A&w?)i literal 0 HcmV?d00001 diff --git a/assets/toolLine.png b/assets/toolLine.png new file mode 100644 index 0000000000000000000000000000000000000000..206b1042bc2237fa368625752e1647d764ec94d5 GIT binary patch literal 371 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qucLCF%=h?3y^w370~qEv>0#LT=By}Z;C1rt33 zJwwYY*EWF^J@#~Q46*QEdf_~mqoc_2kB|3n-NSg(Bgo9`({o0f)f&>Clcsol;u4$W z6d4hFf9Ld_%{%`+xw724_+9?2XC03%>NlFzC+6M?2%KVgF!KA^00FH=t7SKmm;ImV zD6Q4E%&JA1O+)F`zk0ibMUz$}b)0=0?GTZEhFMLkjqS==_L2$CPZZPhN+t_jIq>U7 zQm>Sg{>|rYw?$4qt}7OQC0UsLE%EW)IY%Y361b;)DEd5i{|C0PgEG@^hrhj>$^KDX zNmr-v5QpRoFk5?t`S%HC&hLM4Pn5moF|*Ns&Dr>f+4qZ|aODR1c;0SVmjU!HgQu&X J%Q~loCIHE-mH+?% literal 0 HcmV?d00001 diff --git a/assets/toolRect.png b/assets/toolRect.png new file mode 100644 index 0000000000000000000000000000000000000000..851cb4a2265a11b60255b40a28e2baf7e4b10430 GIT binary patch literal 220 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qucLCF%=h?3y^w370~qEv>0#LT=By}Z;C1rt33 zJwwYY*EWF^`FOfGhFJJ-?eXMdFyJ`y>%YH$wPx!|#%(O!&{DXWlJQt*7HB} zt7vcfz{mPfmg|tUNK3f}$NK<*dkYoz+;*Avo!^hMzw(L|$HLhCbJoNy0Gi9->FVdQ I&MBb@0EFE|ApigX literal 0 HcmV?d00001 diff --git a/assets/toolText.png b/assets/toolText.png new file mode 100644 index 0000000000000000000000000000000000000000..d27b9690570d46f88fbbdab0ea22ba7531fe2758 GIT binary patch literal 306 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qucLCF%=h?3y^w370~qEv>0#LT=By}Z;C1rt33 zJwwYY*EWF^E%J1646*QEI`JXbAp?PC_23r8SN!+?e`Sex__kV2b$eRTsa1{Zr=R}H zmF%qfRR7Y3c9S=Xy!#nHuDEiqJ-YMhtHTzTd^T2!{yTB% w_5wD3zM~oYboZp5X5K%k&34JdvQr Date: Tue, 9 Jan 2018 00:21:43 +0100 Subject: [PATCH 088/148] Initial release w/ circle, line, rectangle, and text tools. --- MiRCARTCanvas.py | 291 +++++++++++++++++----------------------- MiRCARTCanvasBackend.py | 153 +++++++++++++++++++++ MiRCARTCanvasJournal.py | 116 ++++++---------- MiRCARTFrame.py | 85 +++++++----- MiRCARTToPngFile.py | 2 +- MiRCARTTool.py | 6 +- MiRCARTToolRect.py | 5 +- MiRCARTToolText.py | 45 ++++--- 8 files changed, 413 insertions(+), 290 deletions(-) create mode 100644 MiRCARTCanvasBackend.py diff --git a/MiRCARTCanvas.py b/MiRCARTCanvas.py index eb341dd..a3c6642 100644 --- a/MiRCARTCanvas.py +++ b/MiRCARTCanvas.py @@ -22,6 +22,7 @@ # SOFTWARE. # +from MiRCARTCanvasBackend import MiRCARTCanvasBackend from MiRCARTCanvasJournal import MiRCARTCanvasJournal from MiRCARTCanvasStore import MiRCARTCanvasStore, haveMiRCARTToPngFile, haveUrllib from MiRCARTColours import MiRCARTColours @@ -30,208 +31,168 @@ import wx class MiRCARTCanvas(wx.Panel): """XXX""" parentFrame = None - canvasPos = canvasSize = canvasWinSize = cellSize = None - canvasBitmap = canvasMap = None + canvasMap = canvasPos = canvasSize = None brushColours = brushPos = brushSize = None - mircBrushes = mircPens = None - canvasJournal = canvasStore = None - canvasCurTool = None + canvasBackend = canvasCurTool = canvasJournal = canvasStore = None - # {{{ _initBrushesAndPens(self): XXX - def _initBrushesAndPens(self): - self.mircBrushes = [None for x in range(len(MiRCARTColours))] - self.mircPens = [None for x in range(len(MiRCARTColours))] - for mircColour in range(len(MiRCARTColours)): - self.mircBrushes[mircColour] = wx.Brush( \ - wx.Colour(MiRCARTColours[mircColour][0:4]), wx.BRUSHSTYLE_SOLID) - self.mircPens[mircColour] = wx.Pen( \ - wx.Colour(MiRCARTColours[mircColour][0:4]), 1) + # {{{ _commitPatch(self, patch): + def _commitPatch(self, patch): + self.canvasMap[patch[0][1]][patch[0][0]] = patch[1:] # }}} - # {{{ _drawPatch(self, patch, eventDc, tmpDc, atPoint): XXX - def _drawPatch(self, patch, eventDc, tmpDc, atPoint): - absPoint = self._relMapPointToAbsPoint(patch[0], atPoint) - if patch[3] == " ": - brushFg = self.mircBrushes[patch[1][1]] - brushBg = self.mircBrushes[patch[1][0]] - pen = self.mircPens[patch[1][1]] - else: - brushFg = self.mircBrushes[patch[1][0]] - brushBg = self.mircBrushes[patch[1][1]] - pen = self.mircPens[patch[1][0]] - for dc in (eventDc, tmpDc): - dc.SetBrush(brushFg); dc.SetBackground(brushBg); dc.SetPen(pen); - dc.DrawRectangle(*absPoint, *self.cellSize) - # }}} - # {{{ _eventPointToMapPoint(self, eventPoint): XXX - def _eventPointToMapPoint(self, eventPoint): - rectX = eventPoint.x - (eventPoint.x % self.cellSize[0]) - rectY = eventPoint.y - (eventPoint.y % self.cellSize[1]) - mapX = int(rectX / self.cellSize[0] if rectX else 0) - mapY = int(rectY / self.cellSize[1] if rectY else 0) - return (mapX, mapY) - # }}} - # {{{ _getMapCell(self, absMapPoint): XXX - def _getMapCell(self, absMapPoint): - return self.canvasMap[absMapPoint[1]][absMapPoint[0]] - # }}} - # {{{ _relMapPointToAbsPoint(self, relMapPoint, atPoint): XXX - def _relMapPointToAbsPoint(self, relMapPoint, atPoint): - return [(a+b)*c for a,b,c in zip(atPoint, relMapPoint, self.cellSize)] - # }}} - # {{{ _setMapCell(self, absMapPoint, colours, charAttrs, char): XXX - def _setMapCell(self, absMapPoint, colours, charAttrs, char): - self.canvasMap[absMapPoint[1]][absMapPoint[0]] = [colours, charAttrs, char] + # {{{ _dispatchInput(self, eventDc, patches, tmpDc): XXX + def _dispatchInput(self, eventDc, patches, tmpDc): + self.canvasBackend.drawCursorMaskWithJournal( \ + self.canvasJournal, eventDc, tmpDc) + cursorPatches = []; undoPatches = []; newPatches = []; + for patchDescr in patches: + patchIsCursor = patchDescr[0] + for patch in patchDescr[1]: + if self.canvasBackend.drawPatch(eventDc, patch, tmpDc) == False: + continue + else: + patchDeltaCell = self.canvasMap[patch[0][1]][patch[0][0]] + if patchIsCursor == True: + cursorPatches.append([list(patch[0]), *patchDeltaCell.copy()]) + else: + undoPatches.append([list(patch[0]), *patchDeltaCell.copy()]) + newPatches.append(patch) + self._commitPatch(patch) + if len(cursorPatches): + self.canvasJournal.pushCursor(cursorPatches) + if len(undoPatches): + self.canvasJournal.pushDeltas(undoPatches, newPatches) # }}} - # {{{ onClose(self, event): XXX - def onClose(self, event): - self.Destroy(); self.__del__(); + # {{{ onPanelClose(self, event): XXX + def onPanelClose(self, event): + self.Destroy() # }}} - # {{{ onJournalUpdate(self, isTmp, absMapPoint, patch, eventDc, tmpDc, atPoint): - def onJournalUpdate(self, isTmp, absMapPoint, patch, eventDc, tmpDc, atPoint, isInherit=False): - if eventDc == None: - eventDc = wx.ClientDC(self); tmpDc = wx.MemoryDC(); - if tmpDc == None: - tmpDc.SelectObject(self.canvasBitmap) - if isTmp == True: - if isInherit: - patch[1:] = self._getMapCell(patch[0]) - self._drawPatch(patch, eventDc, tmpDc, atPoint) + # {{{ onPanelKeyboardInput(self, event): XXX + def onPanelKeyboardInput(self, event): + eventDc, tmpDc = self.canvasBackend.getDeviceContexts(self) + tool = self.canvasCurTool; mapPoint = self.brushPos; + if event.GetModifiers() != wx.MOD_NONE: + event.Skip() else: - if isInherit: - patchOld = patch.copy() - patchOld[1:] = self._getMapCell(patchOld[0]) - self._setMapCell(absMapPoint, *patch[1:]) - self._drawPatch(patch, eventDc, tmpDc, atPoint) - if isInherit: - return patchOld + patches = tool.onKeyboardEvent( \ + event, mapPoint, self.brushColours, self.brushSize, \ + chr(event.GetUnicodeKey())) + if len(patches): + self._dispatchInput(eventDc, patches, tmpDc) + self.parentFrame.onCanvasUpdate() # }}} - # {{{ onMouseEvent(self, event): XXX - def onMouseEvent(self, event): - eventObject = event.GetEventObject() - eventDc = wx.ClientDC(self); tmpDc = wx.MemoryDC(); - tmpDc.SelectObject(self.canvasBitmap) - eventPoint = event.GetLogicalPosition(eventDc) - mapPoint = self._eventPointToMapPoint(eventPoint) + # {{{ onPanelMouseInput(self, event): XXX + def onPanelMouseInput(self, event): + eventDc, tmpDc = self.canvasBackend.getDeviceContexts(self) tool = self.canvasCurTool - mapPatches = tool.onMouseEvent( \ - event, mapPoint, self.brushColours, self.brushSize, \ + self.brushPos = mapPoint = \ + self.canvasBackend.xlateEventPoint(event, eventDc) + patches = tool.onMouseEvent( \ + event, mapPoint, self.brushColours, self.brushSize, \ event.Dragging(), event.LeftIsDown(), event.RightIsDown()) - self.canvasJournal.merge(mapPatches, eventDc, tmpDc, mapPoint) - self.parentFrame.onCanvasUpdate() + if len(patches): + self._dispatchInput(eventDc, patches, tmpDc) + self.parentFrame.onCanvasUpdate() self.parentFrame.onCanvasMotion(event, mapPoint) # }}} - # {{{ onMouseWindowEvent(self, event): XXX - def onMouseWindowEvent(self, event): - eventObject = event.GetEventObject() - eventDc = wx.ClientDC(self); tmpDc = wx.MemoryDC(); - tmpDc.SelectObject(self.canvasBitmap) - self.canvasJournal.resetCursor(eventDc, tmpDc) + # {{{ onPanelFocus(self, event): XXX + def onPanelFocus(self, event): + if event.GetEventType() == wx.wxEVT_LEAVE_WINDOW: + eventDc, tmpDc = \ + self.canvasBackend.getDeviceContexts(self) + self.canvasBackend.drawCursorMaskWithJournal( \ + self.canvasJournal, eventDc, tmpDc) self.parentFrame.onCanvasMotion(event) # }}} - # {{{ onPaint(self, event): XXX - def onPaint(self, event): - eventDc = wx.BufferedPaintDC(self, self.canvasBitmap) + # {{{ onPanelPaint(self, event): XXX + def onPanelPaint(self, event): + if self.canvasBackend.canvasBitmap != None: + eventDc = wx.BufferedPaintDC(self, self.canvasBackend.canvasBitmap) # }}} # {{{ onStoreUpdate(self, newCanvasSize, newCanvas=None): def onStoreUpdate(self, newCanvasSize, newCanvas=None): - if newCanvasSize != None: - self.resize(newCanvasSize) - self.canvasJournal.reset() - if newCanvas != None: - self.canvasMap = newCanvas.copy() - for numRow in range(self.canvasSize[1]): - numRowCols = len(self.canvasMap[numRow]) - if numRowCols < self.canvasSize[0]: - colsDelta = self.canvasSize[0] - numRowCols - self.canvasMap[numRow][self.canvasSize[0]:] = \ - [[(1, 1), 0, " "] for y in range(colsDelta)] - else: - del self.canvasMap[numRow][self.canvasSize[0]:] - else: - self.canvasMap = [[[(1, 1), 0, " "] \ - for x in range(self.canvasSize[0])] \ - for y in range(self.canvasSize[1])] - canvasWinSize = [a*b for a,b in zip(self.canvasSize, self.cellSize)] - if self.canvasBitmap != None: - self.canvasBitmap.Destroy() - self.canvasBitmap = wx.Bitmap(canvasWinSize) - eventDc = wx.ClientDC(self); tmpDc = wx.MemoryDC(); - tmpDc.SelectObject(self.canvasBitmap) + self.resize(newCanvasSize=newCanvasSize) + self.canvasBackend.reset(self.canvasSize, self.canvasBackend.cellSize) + self.canvasJournal.resetCursor(); self.canvasJournal.resetUndo(); + self.canvasMap = [[[(1, 1), 0, " "] \ + for x in range(self.canvasSize[0])] \ + for y in range(self.canvasSize[1])] + eventDc, tmpDc = self.canvasBackend.getDeviceContexts(self) for numRow in range(self.canvasSize[1]): for numCol in range(self.canvasSize[0]): - self.onJournalUpdate(False, \ - (numCol, numRow), \ - [(numCol, numRow), \ - *self.canvasMap[numRow][numCol]], \ - eventDc, tmpDc, (0, 0)) + if newCanvas != None \ + and numRow < len(newCanvas) \ + and numCol < len(newCanvas[numRow]): + self._commitPatch([ \ + [numCol, numRow], *newCanvas[numRow][numCol]]) + self.canvasBackend.drawPatch(eventDc, \ + ([numCol, numRow], \ + *self.canvasMap[numRow][numCol]), tmpDc) wx.SafeYield() # }}} - # {{{ redo(self): XXX - def redo(self): - result = self.canvasJournal.redo() + # {{{ popRedo(self): + def popRedo(self): + eventDc, tmpDc = self.canvasBackend.getDeviceContexts(self) + patches = self.canvasJournal.popRedo() + for patch in patches: + if self.canvasBackend.drawPatch(eventDc, patch, tmpDc) == False: + continue + else: + self._commitPatch(patch) self.parentFrame.onCanvasUpdate() - return result # }}} - # {{{ resize(self, newCanvasSize): XXX + # {{{ popUndo(self): + def popUndo(self): + eventDc, tmpDc = self.canvasBackend.getDeviceContexts(self) + patches = self.canvasJournal.popUndo() + for patch in patches: + if self.canvasBackend.drawPatch(eventDc, patch, tmpDc) == False: + continue + else: + self._commitPatch(patch) + self.parentFrame.onCanvasUpdate() + # }}} + # {{{ resize(self, newCanvasSize): def resize(self, newCanvasSize): if newCanvasSize != self.canvasSize: - self.SetSize(*self.canvasPos, \ - newCanvasSize[0] * self.cellSize[0], \ - newCanvasSize[1] * self.cellSize[1]) + winSize = [a*b for a,b in \ + zip(newCanvasSize, self.canvasBackend.cellSize)] + self.SetSize(*self.canvasPos, *winSize) for numRow in range(self.canvasSize[1]): for numNewCol in range(self.canvasSize[0], newCanvasSize[0]): - self.canvasMap[numRow].append([1, 1, " "]) + self.canvasMap[numRow].append([[1, 1], 0, " "]) for numNewRow in range(self.canvasSize[1], newCanvasSize[1]): self.canvasMap.append([]) for numNewCol in range(newCanvasSize[0]): - self.canvasMap[numNewRow].append([1, 1, " "]) + self.canvasMap[numNewRow].append([[1, 1], 0, " "]) self.canvasSize = newCanvasSize - canvasWinSize = ( \ - self.cellSize[0] * self.canvasSize[0], \ - self.cellSize[1] * self.canvasSize[1]) - self.canvasBitmap = wx.Bitmap(canvasWinSize) - # }}} - # {{{ undo(self): XXX - def undo(self): - result = self.canvasJournal.undo() + self.canvasBackend.reset(self.canvasSize, \ + self.canvasBackend.cellSize) self.parentFrame.onCanvasUpdate() - return result - # }}} - - # {{{ __del__(self): destructor method - def __del__(self): - if self.canvasBitmap != None: - self.canvasBitmap.Destroy(); self.canvasBitmap = None; - for brush in self.mircBrushes or []: - brush.Destroy() - self.mircBrushes = None - for pen in self.mircPens or []: - pen.Destroy() - self.mircPens = None # }}} # # _init__(self, parent, parentFrame, canvasPos, canvasSize, cellSize): initialisation method def __init__(self, parent, parentFrame, canvasPos, canvasSize, cellSize): - self.parentFrame = parentFrame - self.canvasPos = canvasPos; self.canvasSize = canvasSize; - self.cellSize = cellSize - - self.brushColours = [4, 1]; self._initBrushesAndPens(); - self.brushPos = [0, 0]; self.brushSize = [1, 1]; - self.canvasJournal = MiRCARTCanvasJournal(parentCanvas=self) - self.canvasStore = MiRCARTCanvasStore(parentCanvas=self) - self.canvasCurTool = None - - super().__init__(parent, pos=canvasPos, \ + super().__init__(parent, pos=canvasPos, \ size=[w*h for w,h in zip(canvasSize, cellSize)]) - self.Bind(wx.EVT_CLOSE, self.onClose) - self.Bind(wx.EVT_ENTER_WINDOW, self.onMouseWindowEvent) - self.Bind(wx.EVT_LEAVE_WINDOW, self.onMouseWindowEvent) - self.Bind(wx.EVT_LEFT_DOWN, self.onMouseEvent) - self.Bind(wx.EVT_MOTION, self.onMouseEvent) - self.Bind(wx.EVT_PAINT, self.onPaint) - self.Bind(wx.EVT_RIGHT_DOWN, self.onMouseEvent) + + self.parentFrame = parentFrame + self.canvasMap = None; self.canvasPos = canvasPos; self.canvasSize = canvasSize; + self.brushColours = [4, 1]; self.brushPos = [0, 0]; self.brushSize = [1, 1]; + self.canvasBackend = MiRCARTCanvasBackend(canvasSize, cellSize) + self.canvasCurTool = None + self.canvasJournal = MiRCARTCanvasJournal() + self.canvasStore = MiRCARTCanvasStore(parentCanvas=self) + + # Bind event handlers + self.Bind(wx.EVT_CLOSE, self.onPanelClose) + self.Bind(wx.EVT_ENTER_WINDOW, self.onPanelFocus) + self.Bind(wx.EVT_LEAVE_WINDOW, self.onPanelFocus) + self.parentFrame.Bind(wx.EVT_CHAR, self.onPanelKeyboardInput) + for eventType in( \ + wx.EVT_LEFT_DOWN, wx.EVT_MOTION, wx.EVT_RIGHT_DOWN): + self.Bind(eventType, self.onPanelMouseInput) + self.Bind(wx.EVT_PAINT, self.onPanelPaint) # vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/MiRCARTCanvasBackend.py b/MiRCARTCanvasBackend.py new file mode 100644 index 0000000..b9381f9 --- /dev/null +++ b/MiRCARTCanvasBackend.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +# +# MiRCARTCanvasBackend.py -- XXX +# Copyright (c) 2018 Lucio Andrés Illanes Albornoz +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +from MiRCARTColours import MiRCARTColours +import wx + +class MiRCARTCanvasBackend(): + """XXX""" + _font = _brushes = _pens = None + canvasBitmap = cellSize = None + + # {{{ _drawBrushPatch(self, eventDc, patch, tmpDc): XXX + def _drawBrushPatch(self, eventDc, patch, tmpDc): + absPoint = self._xlatePoint(patch[0]) + brushFg = self._brushes[patch[1][1]] + brushBg = self._brushes[patch[1][0]] + pen = self._pens[patch[1][1]] + for dc in (eventDc, tmpDc): + dc.SetBrush(brushFg); dc.SetBackground(brushBg); dc.SetPen(pen); + dc.DrawRectangle(*absPoint, *self.cellSize) + # }}} + # {{{ _drawCharPatch(self, eventDc, patch, tmpDc): XXX + def _drawCharPatch(self, eventDc, patch, tmpDc): + absPoint = self._xlatePoint(patch[0]) + brushFg = self._brushes[patch[1][0]] + brushBg = self._brushes[patch[1][1]] + pen = self._pens[patch[1][1]] + for dc in (eventDc, tmpDc): + fontBitmap = wx.Bitmap(*self.cellSize) + fontDc = wx.MemoryDC(); fontDc.SelectObject(fontBitmap); + fontDc.SetTextForeground(wx.Colour(MiRCARTColours[patch[1][0]][0:4])) + fontDc.SetTextBackground(wx.Colour(MiRCARTColours[patch[1][1]][0:4])) + fontDc.SetBrush(brushBg); fontDc.SetBackground(brushBg); fontDc.SetPen(pen); + fontDc.SetFont(self._font) + fontDc.DrawRectangle(0, 0, *self.cellSize) + fontDc.DrawText(patch[3], 0, 0) + dc.Blit(*absPoint, *self.cellSize, fontDc, 0, 0) + # }}} + # {{{ _finiBrushesAndPens(self): XXX + def _finiBrushesAndPens(self): + for brush in self._brushes or []: + brush.Destroy() + self._brushes = None + for pen in self._pens or []: + pen.Destroy() + self._pens = None + # }}} + # {{{ _initBrushesAndPens(self): XXX + def _initBrushesAndPens(self): + self._brushes = [None for x in range(len(MiRCARTColours))] + self._pens = [None for x in range(len(MiRCARTColours))] + for mircColour in range(len(MiRCARTColours)): + self._brushes[mircColour] = wx.Brush( \ + wx.Colour(MiRCARTColours[mircColour][0:4]), wx.BRUSHSTYLE_SOLID) + self._pens[mircColour] = wx.Pen( \ + wx.Colour(MiRCARTColours[mircColour][0:4]), 1) + # }}} + # {{{ _xlatePoint(self, relMapPoint): XXX + def _xlatePoint(self, relMapPoint): + return [a*b for a,b in zip(relMapPoint, self.cellSize)] + # }}} + + # {{{ drawPatch(self, eventDc, patch, tmpDc): XXX + def drawPatch(self, eventDc, patch, tmpDc): + if patch[0][0] < self.canvasSize[0] \ + and patch[0][1] < self.canvasSize[1]: + if patch[3] == " ": + self._drawBrushPatch(eventDc, patch, tmpDc) + else: + self._drawCharPatch(eventDc, patch, tmpDc) + return True + else: + return False + # }}} + # {{{ drawCursorMaskWithJournal(self, canvasJournal, eventDc, tmpDc): XXX + def drawCursorMaskWithJournal(self, canvasJournal, eventDc, tmpDc): + for patch in canvasJournal.popCursor(): + self.drawPatch(eventDc, patch, tmpDc) + # }}} + # {{{ getDeviceContexts(self, parentWindow): XXX + def getDeviceContexts(self, parentWindow): + eventDc = wx.ClientDC(parentWindow); tmpDc = wx.MemoryDC(); + tmpDc.SelectObject(self.canvasBitmap) + return (eventDc, tmpDc) + # }}} + # {{{ reset(self, canvasSize, cellSize): + def reset(self, canvasSize, cellSize): + self.resize(canvasSize, cellSize) + # }}} + # {{{ resize(self, canvasSize, cellSize): + def resize(self, canvasSize, cellSize): + winSize = [a*b for a,b in zip(canvasSize, cellSize)] + if self.canvasBitmap == None: + self.canvasBitmap = wx.Bitmap(winSize) + else: + oldDc = wx.MemoryDC() + oldDc.SelectObject(self.canvasBitmap) + newDc = wx.MemoryDC() + newBitmap = wx.Bitmap(winSize) + newDc.SelectObject(newBitmap) + newDc.Blit(0, 0, *self.canvasBitmap.GetSize(), oldDc, 0, 0) + self.canvasBitmap = newBitmap + self.canvasSize = canvasSize; self.cellSize = cellSize; + self._font = wx.Font( \ + 8, \ + wx.FONTFAMILY_TELETYPE, \ + wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL) + # }}} + # {{{ xlateEventPoint(self, event, eventDc): XXX + def xlateEventPoint(self, event, eventDc): + eventPoint = event.GetLogicalPosition(eventDc) + rectX = eventPoint.x - (eventPoint.x % self.cellSize[0]) + rectY = eventPoint.y - (eventPoint.y % self.cellSize[1]) + mapX = int(rectX / self.cellSize[0] if rectX else 0) + mapY = int(rectY / self.cellSize[1] if rectY else 0) + return (mapX, mapY) + # }}} + + # {{{ __del__(self): destructor method + def __del__(self): + if self.canvasBitmap != None: + self.canvasBitmap.Destroy(); self.canvasBitmap = None; + self._finiBrushesAndPens() + # }}} + + # + # _init__(self, canvasSize, cellSize): initialisation method + def __init__(self, canvasSize, cellSize): + self._initBrushesAndPens() + self.reset(canvasSize, cellSize) + +# vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/MiRCARTCanvasJournal.py b/MiRCARTCanvasJournal.py index 9ac34d2..17706e7 100644 --- a/MiRCARTCanvasJournal.py +++ b/MiRCARTCanvasJournal.py @@ -24,94 +24,58 @@ class MiRCARTCanvasJournal(): """XXX""" - parentCanvas = None - patchesTmp = patchesUndo = patchesUndoLevel = None + patchesCursor = patchesUndo = patchesUndoLevel = None - # {{{ _popTmp(self, eventDc, tmpDc): XXX - def _popTmp(self, eventDc, tmpDc): - if self.patchesTmp: - for patch in self.patchesTmp: - self.parentCanvas.onJournalUpdate(True, \ - patch[0], patch, eventDc, tmpDc, (0, 0), True) - self.patchesTmp = [] + # {{{ popCursor(self): XXX + def popCursor(self): + if len(self.patchesCursor): + patchesCursor = self.patchesCursor + self.patchesCursor = [] + return patchesCursor + else: + return [] # }}} - # {{{ _pushTmp(self, atPoint): XXX - def _pushTmp(self, atPoint): - self.patchesTmp.append([atPoint, None, None, None]) - # }}} - # {{{ _pushUndo(self, atPoint, patches): XXX - def _pushUndo(self, atPoint, patches): - if self.patchesUndoLevel > 0: - del self.patchesUndo[0:self.patchesUndoLevel] - self.patchesUndoLevel = 0 - patchesUndo = [] - for patch in patches: - patchesUndo.append([ \ - [patch[0], *patch[0][1:]], \ - [patch[0], *patch[1][1:]]]) - if len(patchesUndo) > 0: - self.patchesUndo.insert(0, patchesUndo) - # }}} - # {{{ merge(self, mapPatches, eventDc, tmpDc, atPoint): XXX - def merge(self, mapPatches, eventDc, tmpDc, atPoint): - patchesUndo = [] - for mapPatch in mapPatches: - mapPatchTmp = mapPatch[0] - if mapPatchTmp: - self._popTmp(eventDc, tmpDc) - for patch in mapPatch[1]: - if patch[0][0] >= self.parentCanvas.canvasSize[0] \ - or patch[0][1] >= self.parentCanvas.canvasSize[1]: - continue - elif mapPatchTmp: - self._pushTmp(patch[0]) - self.parentCanvas.onJournalUpdate(mapPatchTmp, \ - patch[0], patch, eventDc, tmpDc, (0, 0)) - else: - patchUndo = \ - self.parentCanvas.onJournalUpdate(mapPatchTmp, \ - patch[0], patch, eventDc, tmpDc, (0, 0), True) - patchesUndo.append([patchUndo, patch]) - if len(patchesUndo) > 0: - self._pushUndo(atPoint, patchesUndo) - # }}} - # {{{ redo(self): XXX - def redo(self): + # {{{ popRedo(self): XXX + def popRedo(self): if self.patchesUndoLevel > 0: self.patchesUndoLevel -= 1 - for patch in self.patchesUndo[self.patchesUndoLevel]: - self.parentCanvas.onJournalUpdate(False, \ - patch[1][0], patch[1], None, None, (0, 0)) - return True + patches = self.patchesUndo[self.patchesUndoLevel] + return patches[1] else: - return False + return [] # }}} - # {{{ reset(self): XXX - def reset(self): - self.patchesTmp = []; self.patchesUndo = [None]; self.patchesUndoLevel = 0; - # }}} - # {{{ resetCursor(self, eventDc, tmpDc): XXX - def resetCursor(self, eventDc, tmpDc): - if len(self.patchesTmp): - self._popTmp(eventDc, tmpDc) - self.patchesTmp = [] - # }}} - # {{{ undo(self): XXX - def undo(self): + # {{{ popUndo(self): XXX + def popUndo(self): if self.patchesUndo[self.patchesUndoLevel] != None: patches = self.patchesUndo[self.patchesUndoLevel] self.patchesUndoLevel += 1 - for patch in patches: - self.parentCanvas.onJournalUpdate(False, \ - patch[0][0], patch[0], None, None, (0, 0)) - return True + return patches[0] else: - return False + return [] + # }}} + # {{{ pushCursor(self, patches): XXX + def pushCursor(self, patches): + self.patchesCursor += patches + # }}} + # {{{ pushDeltas(self, undoPatches, redoPatches): XXX + def pushDeltas(self, undoPatches, redoPatches): + if self.patchesUndoLevel > 0: + del self.patchesUndo[0:self.patchesUndoLevel] + self.patchesUndoLevel = 0 + self.patchesUndo.insert(0, [undoPatches, redoPatches]) + # }}} + # {{{ resetCursor(self): XXX + def resetCursor(self): + self.patchesCursor = [] + # }}} + # {{{ resetUndo(self): XXX + def resetUndo(self): + self.patchesUndo = [None]; self.patchesUndoLevel = 0; # }}} # - # __init__(self, parentCanvas): initialisation method - def __init__(self, parentCanvas): - self.parentCanvas = parentCanvas; self.reset(); + # __init__(self): initialisation method + def __init__(self): + self.resetCursor(); self.resetUndo(); # vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/MiRCARTFrame.py b/MiRCARTFrame.py index 1e14a7d..a3a870b 100644 --- a/MiRCARTFrame.py +++ b/MiRCARTFrame.py @@ -36,7 +36,6 @@ import os, wx class MiRCARTFrame(MiRCARTGeneralFrame): """XXX""" panelCanvas = canvasPathName = None - canvasPos = canvasSize = cellSize = None # {{{ Commands # Id Type Id Labels Icon bitmap Accelerator [Initial state] @@ -57,11 +56,15 @@ class MiRCARTFrame(MiRCARTGeneralFrame): CID_COPY = [0x10b, TID_COMMAND, "Copy", "&Copy", ["", wx.ART_COPY], None, False] CID_PASTE = [0x10c, TID_COMMAND, "Paste", "&Paste", ["", wx.ART_PASTE], None, False] CID_DELETE = [0x10d, TID_COMMAND, "Delete", "De&lete", ["", wx.ART_DELETE], None, False] - CID_INCRBRUSH = [0x10e, TID_COMMAND, "Increase brush size", \ + CID_INCR_CANVAS = [0x10e, TID_COMMAND, "Increase canvas size", \ + "&Increase canvas size", ["", wx.ART_PLUS], [wx.ACCEL_SHIFT, ord("+")]] + CID_DECR_CANVAS = [0x10f, TID_COMMAND, "Decrease brush size", \ + "&Decrease brush size", ["", wx.ART_MINUS], [wx.ACCEL_SHIFT, ord("-")]] + CID_INCR_BRUSH = [0x110, TID_COMMAND, "Increase brush size", \ "&Increase brush size", ["", wx.ART_PLUS], [wx.ACCEL_CTRL, ord("+")]] - CID_DECRBRUSH = [0x10f, TID_COMMAND, "Decrease brush size", \ + CID_DECR_BRUSH = [0x111, TID_COMMAND, "Decrease brush size", \ "&Decrease brush size", ["", wx.ART_MINUS], [wx.ACCEL_CTRL, ord("-")]] - CID_SOLID_BRUSH = [0x110, TID_SELECT, "Solid brush", "&Solid brush", None, None, True] + CID_SOLID_BRUSH = [0x112, TID_SELECT, "Solid brush", "&Solid brush", None, None, True] CID_RECT = [0x150, TID_SELECT, "Rectangle", "&Rectangle", ["toolRect.png"], [wx.ACCEL_CTRL, ord("R")], True] CID_CIRCLE = [0x151, TID_SELECT, "Circle", "&Circle", ["toolCircle.png"], [wx.ACCEL_CTRL, ord("C")], False] @@ -97,7 +100,8 @@ class MiRCARTFrame(MiRCARTGeneralFrame): MID_EDIT = (0x301, TID_MENU, "Edit", "&Edit", ( \ CID_UNDO, CID_REDO, NID_MENU_SEP, \ CID_CUT, CID_COPY, CID_PASTE, CID_DELETE, NID_MENU_SEP, \ - CID_INCRBRUSH, CID_DECRBRUSH, CID_SOLID_BRUSH)) + CID_INCR_CANVAS, CID_DECR_CANVAS, NID_MENU_SEP, \ + CID_INCR_BRUSH, CID_DECR_BRUSH, CID_SOLID_BRUSH)) MID_TOOLS = (0x302, TID_MENU, "Tools", "&Tools", ( \ CID_RECT, CID_CIRCLE, CID_LINE, CID_TEXT)) # }}} @@ -106,7 +110,7 @@ class MiRCARTFrame(MiRCARTGeneralFrame): CID_NEW, CID_OPEN, CID_SAVE, CID_SAVEAS, NID_TOOLBAR_SEP, \ CID_UNDO, CID_REDO, NID_TOOLBAR_SEP, \ CID_CUT, CID_COPY, CID_PASTE, CID_DELETE, NID_TOOLBAR_SEP, \ - CID_INCRBRUSH, CID_DECRBRUSH, CID_SOLID_BRUSH, NID_TOOLBAR_SEP, \ + CID_INCR_BRUSH, CID_DECR_BRUSH, NID_TOOLBAR_SEP, \ CID_RECT, CID_CIRCLE, CID_LINE, CID_TEXT, NID_TOOLBAR_SEP, \ CID_COLOUR00, CID_COLOUR01, CID_COLOUR02, CID_COLOUR03, CID_COLOUR04, \ CID_COLOUR05, CID_COLOUR06, CID_COLOUR07, CID_COLOUR08, CID_COLOUR09, \ @@ -115,7 +119,8 @@ class MiRCARTFrame(MiRCARTGeneralFrame): # }}} # {{{ Accelerators (hotkeys) AID_EDIT = (0x500, TID_ACCELS, ( \ - CID_NEW, CID_OPEN, CID_SAVE, CID_UNDO, CID_REDO, CID_INCRBRUSH, CID_DECRBRUSH)) + CID_NEW, CID_OPEN, CID_SAVE, CID_UNDO, CID_REDO, \ + CID_INCR_CANVAS, CID_INCR_BRUSH, CID_INCR_BRUSH, CID_DECR_BRUSH)) # }}} # {{{ Lists LID_ACCELS = (0x600, TID_LIST, (AID_EDIT)) @@ -177,15 +182,15 @@ class MiRCARTFrame(MiRCARTGeneralFrame): # {{{ canvasExportAsPng(self): XXX def canvasExportAsPng(self): - with wx.FileDialog(self, self.CID_SAVEAS[2], os.getcwd(), "", \ + with wx.FileDialog(self, self.CID_SAVEAS[2], os.getcwd(), "", \ "*.png", wx.FD_SAVE|wx.FD_OVERWRITE_PROMPT) as dialog: if dialog.ShowModal() == wx.ID_CANCEL: return False else: outPathName = dialog.GetPath() self.SetCursor(wx.Cursor(wx.CURSOR_WAIT)) - self.panelCanvas.canvasStore.exportBitmapToPngFile( \ - self.panelCanvas.canvasBitmap, outPathName, \ + self.panelCanvas.canvasStore.exportBitmapToPngFile( \ + self.panelCanvas.canvasBackend.canvasBitmap, outPathName, \ wx.BITMAP_TYPE_PNG) self.SetCursor(wx.Cursor(wx.NullCursor)) return True @@ -194,7 +199,8 @@ class MiRCARTFrame(MiRCARTGeneralFrame): def canvasExportImgur(self): self.SetCursor(wx.Cursor(wx.CURSOR_WAIT)) imgurResult = self.panelCanvas.canvasStore.exportBitmapToImgur( \ - "c9a6efb3d7932fd", self.panelCanvas.canvasBitmap, "", "", wx.BITMAP_TYPE_PNG) + "c9a6efb3d7932fd", self.panelCanvas.canvasBackend.canvasBitmap, \ + "", "", wx.BITMAP_TYPE_PNG) self.SetCursor(wx.Cursor(wx.NullCursor)) if imgurResult[0] == 200: if not wx.TheClipboard.IsOpened(): @@ -295,12 +301,12 @@ class MiRCARTFrame(MiRCARTGeneralFrame): return self.canvasSave() # }}} # {{{ onCanvasMotion(self, event): XXX - def onCanvasMotion(self, event, mapPoint=None): + def onCanvasMotion(self, event, atPoint=None): eventType = event.GetEventType() if eventType == wx.wxEVT_ENTER_WINDOW: - pass + self.SetFocus() elif eventType == wx.wxEVT_MOTION: - self._updateStatusBar(showPos=mapPoint) + self._updateStatusBar(showPos=atPoint) elif eventType == wx.wxEVT_LEAVE_WINDOW: pass # }}} @@ -339,9 +345,9 @@ class MiRCARTFrame(MiRCARTGeneralFrame): elif cid == self.CID_EXIT[0]: self.Close(True) elif cid == self.CID_UNDO[0]: - self.panelCanvas.undo() + self.panelCanvas.popUndo() elif cid == self.CID_REDO[0]: - self.panelCanvas.redo() + self.panelCanvas.popRedo() elif cid == self.CID_CUT[0]: pass elif cid == self.CID_COPY[0]: @@ -350,33 +356,52 @@ class MiRCARTFrame(MiRCARTGeneralFrame): pass elif cid == self.CID_DELETE[0]: pass - elif cid == self.CID_INCRBRUSH[0]: - self.panelCanvas.brushSize = \ + elif cid == self.CID_INCR_CANVAS[0] \ + or cid == self.CID_DECR_CANVAS[0]: + eventDc, tmpDc = self.panelCanvas.canvasBackend.getDeviceContexts(self) + if cid == self.CID_INCR_CANVAS[0]: + newCanvasSize = [a+1 for a in self.panelCanvas.canvasSize] + else: + newCanvasSize = [a-1 if a > 1 else a for a in self.panelCanvas.canvasSize] + self.panelCanvas.resize(newCanvasSize) + self.panelCanvas.canvasBackend.resize( \ + newCanvasSize, \ + self.panelCanvas.canvasBackend.cellSize) + for numRow in range(self.panelCanvas.canvasSize[1] - 1): + self.panelCanvas.canvasMap.append([[1, 1], 0, " "]) + self.panelCanvas.canvasMap.append([]) + for numCol in range(self.panelCanvas.canvasSize[0]): + self.panelCanvas.canvasMap[-1].append([[1, 1], 0, " "]) + self.panelCanvas.canvasBackend.drawPatch(eventDc, \ + ([numCol, self.panelCanvas.canvasSize[1] - 1], *[[1, 1], 0, " "]), tmpDc) + wx.SafeYield() + elif cid == self.CID_INCR_BRUSH[0]: + self.panelCanvas.brushSize = \ [a+1 for a in self.panelCanvas.brushSize] - elif cid == self.CID_DECRBRUSH[0] \ - and self.panelCanvas.brushSize[0] > 1 \ + elif cid == self.CID_DECR_BRUSH[0] \ + and self.panelCanvas.brushSize[0] > 1 \ and self.panelCanvas.brushSize[1] > 1: - self.panelCanvas.brushSize = \ + self.panelCanvas.brushSize = \ [a-1 for a in self.panelCanvas.brushSize] elif cid == self.CID_SOLID_BRUSH[0]: pass elif cid == self.CID_RECT[0]: self.menuItemsById[cid].Check(True) - self.panelCanvas.canvasCurTool = \ + self.panelCanvas.canvasCurTool = \ MiRCARTToolRect(self.panelCanvas) elif cid == self.CID_CIRCLE[0]: self.menuItemsById[cid].Check(True) - self.panelCanvas.canvasCurTool = \ + self.panelCanvas.canvasCurTool = \ MiRCARTToolCircle(self.panelCanvas) elif cid == self.CID_LINE[0]: self.menuItemsById[cid].Check(True) - self.panelCanvas.canvasCurTool = \ + self.panelCanvas.canvasCurTool = \ MiRCARTToolLine(self.panelCanvas) elif cid == self.CID_TEXT[0]: self.menuItemsById[cid].Check(True) - self.panelCanvas.canvasCurTool = \ + self.panelCanvas.canvasCurTool = \ MiRCARTToolText(self.panelCanvas) - elif cid >= self.CID_COLOUR00[0] \ + elif cid >= self.CID_COLOUR00[0] \ and cid <= self.CID_COLOUR15[0]: numColour = cid - self.CID_COLOUR00[0] if event.GetEventType() == wx.wxEVT_TOOL: @@ -392,15 +417,13 @@ class MiRCARTFrame(MiRCARTGeneralFrame): # }}} # - # __init__(self, parent, appSize=(800, 600), canvasPos=(25, 50), canvasSize=(100, 30), cellSize=(7, 14)): initialisation method - def __init__(self, parent, appSize=(800, 600), canvasPos=(25, 50), canvasSize=(100, 30), cellSize=(7, 14)): + # __init__(self, parent, appSize=(840, 630), canvasPos=(25, 50), canvasSize=(125, 35), cellSize=(7, 14)): initialisation method + def __init__(self, parent, appSize=(840, 630), canvasPos=(25, 50), canvasSize=(125, 35), cellSize=(7, 14)): self._initPaletteToolBitmaps() panelSkin = super().__init__(parent, wx.ID_ANY, "MiRCART", size=appSize) - self.canvasPos = canvasPos; self.cellSize = cellSize; self.canvasSize = canvasSize; self.canvasPathName = None self.panelCanvas = MiRCARTCanvas(panelSkin, parentFrame=self, \ - canvasPos=self.canvasPos, canvasSize=self.canvasSize, \ - cellSize=self.cellSize) + canvasPos=canvasPos, canvasSize=canvasSize, cellSize=cellSize) self.panelCanvas.canvasCurTool = MiRCARTToolRect(self.panelCanvas) self.canvasNew() diff --git a/MiRCARTToPngFile.py b/MiRCARTToPngFile.py index cf6e7de..78ef788 100755 --- a/MiRCARTToPngFile.py +++ b/MiRCARTToPngFile.py @@ -24,7 +24,7 @@ import MiRCARTCanvasStore from PIL import Image, ImageDraw, ImageFont -import string, sys +import sys class MiRCARTToPngFile: """XXX""" diff --git a/MiRCARTTool.py b/MiRCARTTool.py index ae91e6d..be8758c 100644 --- a/MiRCARTTool.py +++ b/MiRCARTTool.py @@ -26,9 +26,13 @@ class MiRCARTTool(): """XXX""" parentCanvas = None + # {{{ onKeyboardEvent(self, event, atPoint, brushColours, brushSize, keyChar): + def onKeyboardEvent(self, event, atPoint, brushColours, brushSize, keyChar): + return () + # }}} # {{{ onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown): XXX def onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown): - pass + return () # }}} # diff --git a/MiRCARTToolRect.py b/MiRCARTToolRect.py index d2452c5..a4e9662 100644 --- a/MiRCARTToolRect.py +++ b/MiRCARTToolRect.py @@ -37,9 +37,12 @@ class MiRCARTToolRect(MiRCARTTool): brushColours[0] = brushColours[1] else: brushColours[1] = brushColours[0] + brushSize = brushSize.copy() + if brushSize[0] > 1: + brushSize[0] *= 2 brushPatches = [] for brushRow in range(brushSize[1]): - for brushCol in range(brushSize[0] * 2): + for brushCol in range(brushSize[0]): brushPatches.append([[ \ atPoint[0] + brushCol, \ atPoint[1] + brushRow], \ diff --git a/MiRCARTToolText.py b/MiRCARTToolText.py index 9fd18ab..8f1be6d 100644 --- a/MiRCARTToolText.py +++ b/MiRCARTToolText.py @@ -23,30 +23,45 @@ # from MiRCARTTool import MiRCARTTool +import string class MiRCARTToolText(MiRCARTTool): """XXX""" + textColours = textPos = None + + # + # onKeyboardEvent(self, event, atPoint, brushColours, brushSize, keyChar): XXX + def onKeyboardEvent(self, event, atPoint, brushColours, brushSize, keyChar): + if not keyChar in string.printable: + return [] + else: + if self.textColours == None: + self.textColours = brushColours.copy() + if self.textPos == None: + self.textPos = list(atPoint) + patches = [[False, [[self.textPos, self.textColours, 0, keyChar]]]] + if self.textPos[0] < (self.parentCanvas.canvasSize[0] - 1): + self.textPos[0] += 1 + elif self.textPos[1] < (self.parentCanvas.canvasSize[1] - 1): + self.textPos[0] = 0 + self.textPos[1] += 1 + else: + self.textPos = [0, 0] + return patches # # onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown): XXX def onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown): - brushColours = brushColours.copy() if isLeftDown: - brushColours[1] = brushColours[0] + self.textColours = brushColours.copy() + self.textPos = list(atPoint) elif isRightDown: - brushColours[0] = brushColours[1] + self.textColours = [brushColours[1], brushColours[0]] + self.textPos = list(atPoint) else: - brushColours[1] = brushColours[0] - brushPatches = [] - for brushRow in range(brushSize[1]): - for brushCol in range(brushSize[0] * 2): - brushPatches.append([[ \ - atPoint[0] + brushCol, \ - atPoint[1] + brushRow], \ - brushColours, 0, " "]) - if isLeftDown or isRightDown: - return [[False, brushPatches], [True, brushPatches]] - else: - return [[True, brushPatches]] + if self.textColours == None: + self.textColours = brushColours.copy() + self.textPos = list(atPoint) + return [[True, [[self.textPos, self.textColours, 0, "_"]]]] # vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 From dec1e4c0d41706bcb92d8bd7bd42ae0d313d65ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Tue, 9 Jan 2018 04:02:17 +0100 Subject: [PATCH 089/148] MiRCART.png: updates screenshot. MiRCARTCanvas.py:onPanelKeyboardInput(): only handle {no,shift}-modifier keyboard events. MiRCARTFrame.py: fix {de,in}crease canvas size accelerators (hotkeys.) --- MiRCART.png | Bin 35691 -> 43602 bytes MiRCARTCanvas.py | 4 +++- MiRCARTFrame.py | 8 ++++---- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/MiRCART.png b/MiRCART.png index 26cfcdf0a23fb17135c6c8066bd1bb9543941e20..8db9e9391678888bc477891c71e9622f2e83cb58 100644 GIT binary patch literal 43602 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

F@dVhnL0yN8Bo0x&I=| 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 diff --git a/MiRCARTCanvas.py b/MiRCARTCanvas.py index a3c6642..db8d440 100644 --- a/MiRCARTCanvas.py +++ b/MiRCARTCanvas.py @@ -71,7 +71,9 @@ class MiRCARTCanvas(wx.Panel): def onPanelKeyboardInput(self, event): eventDc, tmpDc = self.canvasBackend.getDeviceContexts(self) tool = self.canvasCurTool; mapPoint = self.brushPos; - if event.GetModifiers() != wx.MOD_NONE: + keyModifiers = event.GetModifiers() + if keyModifiers != wx.MOD_NONE \ + and keyModifiers != wx.MOD_SHIFT: event.Skip() else: patches = tool.onKeyboardEvent( \ diff --git a/MiRCARTFrame.py b/MiRCARTFrame.py index a3a870b..b7e5e5d 100644 --- a/MiRCARTFrame.py +++ b/MiRCARTFrame.py @@ -57,9 +57,9 @@ class MiRCARTFrame(MiRCARTGeneralFrame): CID_PASTE = [0x10c, TID_COMMAND, "Paste", "&Paste", ["", wx.ART_PASTE], None, False] CID_DELETE = [0x10d, TID_COMMAND, "Delete", "De&lete", ["", wx.ART_DELETE], None, False] CID_INCR_CANVAS = [0x10e, TID_COMMAND, "Increase canvas size", \ - "&Increase canvas size", ["", wx.ART_PLUS], [wx.ACCEL_SHIFT, ord("+")]] - CID_DECR_CANVAS = [0x10f, TID_COMMAND, "Decrease brush size", \ - "&Decrease brush size", ["", wx.ART_MINUS], [wx.ACCEL_SHIFT, ord("-")]] + "I&ncrease canvas size", ["", wx.ART_PLUS], [wx.ACCEL_ALT, ord("+")]] + CID_DECR_CANVAS = [0x10f, TID_COMMAND, "Decrease canvas size", \ + "D&ecrease canvas size", ["", wx.ART_MINUS], [wx.ACCEL_ALT, ord("-")]] CID_INCR_BRUSH = [0x110, TID_COMMAND, "Increase brush size", \ "&Increase brush size", ["", wx.ART_PLUS], [wx.ACCEL_CTRL, ord("+")]] CID_DECR_BRUSH = [0x111, TID_COMMAND, "Decrease brush size", \ @@ -120,7 +120,7 @@ class MiRCARTFrame(MiRCARTGeneralFrame): # {{{ Accelerators (hotkeys) AID_EDIT = (0x500, TID_ACCELS, ( \ CID_NEW, CID_OPEN, CID_SAVE, CID_UNDO, CID_REDO, \ - CID_INCR_CANVAS, CID_INCR_BRUSH, CID_INCR_BRUSH, CID_DECR_BRUSH)) + CID_INCR_CANVAS, CID_DECR_CANVAS, CID_INCR_BRUSH, CID_DECR_BRUSH)) # }}} # {{{ Lists LID_ACCELS = (0x600, TID_LIST, (AID_EDIT)) From 363a86fe812a23b00774a2205ee9b1a6a4390a2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Tue, 9 Jan 2018 05:07:04 +0100 Subject: [PATCH 090/148] MiRCARTCanvas.py: enable double buffering to eliminate brush flickering. --- MiRCARTCanvas.py | 1 + 1 file changed, 1 insertion(+) diff --git a/MiRCARTCanvas.py b/MiRCARTCanvas.py index db8d440..b2e0021 100644 --- a/MiRCARTCanvas.py +++ b/MiRCARTCanvas.py @@ -178,6 +178,7 @@ class MiRCARTCanvas(wx.Panel): def __init__(self, parent, parentFrame, canvasPos, canvasSize, cellSize): super().__init__(parent, pos=canvasPos, \ size=[w*h for w,h in zip(canvasSize, cellSize)]) + self.SetDoubleBuffered(True) self.parentFrame = parentFrame self.canvasMap = None; self.canvasPos = canvasPos; self.canvasSize = canvasSize; From dbf6c7693093e000266ee583295a8235117ae39d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Tue, 9 Jan 2018 21:19:14 +0100 Subject: [PATCH 091/148] MiRCARTGeneralFrame.py: prepend script execution directory when loading toolbar item bitmaps. --- MiRCARTGeneralFrame.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/MiRCARTGeneralFrame.py b/MiRCARTGeneralFrame.py index 7bdf00a..7274af5 100644 --- a/MiRCARTGeneralFrame.py +++ b/MiRCARTGeneralFrame.py @@ -22,7 +22,7 @@ # SOFTWARE. # -import os, wx +import os, sys, wx # # Types @@ -108,10 +108,11 @@ class MiRCARTGeneralFrame(wx.Frame): and toolBarItem[4][1] == None: toolBarItem[4] = ["", None, toolBarItem[4][2]] elif toolBarItem[4][0] != "": + toolBitmapPathName = os.path.dirname(sys.argv[0]) + toolBitmapPathName = os.path.join(toolBitmapPathName, \ + "assets", toolBarItem[4][0]) toolBitmap = wx.Bitmap((16,16)) - toolBitmap.LoadFile( \ - os.path.join("assets", toolBarItem[4][0]), \ - wx.BITMAP_TYPE_ANY) + toolBitmap.LoadFile(toolBitmapPathName, wx.BITMAP_TYPE_ANY) toolBarItem[4] = ["", None, toolBitmap] # }}} # {{{ onClose(self, event): XXX From 2e014cd7ecaf15c0d909c30fcf5b4dc96a9e4567 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Tue, 9 Jan 2018 21:37:58 +0100 Subject: [PATCH 092/148] MiRCARTCanvasBackend.py: only update device context brushes & pen if necessary. --- MiRCARTCanvasBackend.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/MiRCARTCanvasBackend.py b/MiRCARTCanvasBackend.py index b9381f9..34459db 100644 --- a/MiRCARTCanvasBackend.py +++ b/MiRCARTCanvasBackend.py @@ -28,6 +28,7 @@ import wx class MiRCARTCanvasBackend(): """XXX""" _font = _brushes = _pens = None + _lastBrush = _lastPen = None canvasBitmap = cellSize = None # {{{ _drawBrushPatch(self, eventDc, patch, tmpDc): XXX @@ -36,9 +37,9 @@ class MiRCARTCanvasBackend(): brushFg = self._brushes[patch[1][1]] brushBg = self._brushes[patch[1][0]] pen = self._pens[patch[1][1]] - for dc in (eventDc, tmpDc): - dc.SetBrush(brushFg); dc.SetBackground(brushBg); dc.SetPen(pen); - dc.DrawRectangle(*absPoint, *self.cellSize) + self._setBrushDc(brushBg, brushFg, (eventDc, tmpDc), pen) + eventDc.DrawRectangle(*absPoint, *self.cellSize) + tmpDc.DrawRectangle(*absPoint, *self.cellSize) # }}} # {{{ _drawCharPatch(self, eventDc, patch, tmpDc): XXX def _drawCharPatch(self, eventDc, patch, tmpDc): @@ -65,6 +66,7 @@ class MiRCARTCanvasBackend(): for pen in self._pens or []: pen.Destroy() self._pens = None + self._lastBrushBg = self._lastBrushFg = self._lastPen = None; # }}} # {{{ _initBrushesAndPens(self): XXX def _initBrushesAndPens(self): @@ -75,6 +77,22 @@ class MiRCARTCanvasBackend(): wx.Colour(MiRCARTColours[mircColour][0:4]), wx.BRUSHSTYLE_SOLID) self._pens[mircColour] = wx.Pen( \ wx.Colour(MiRCARTColours[mircColour][0:4]), 1) + self._lastBrushBg = self._lastBrushFg = self._lastPen = None; + # }}} + # {{{ _setBrushDc(self, brushBg, brushFg, dcList, pen): XXX + def _setBrushDc(self, brushBg, brushFg, dcList, pen): + if self._lastBrushBg != brushBg: + for dc in dcList: + dc.SetBackground(brushBg) + self._lastBrushBg = brushBg + if self._lastBrushFg != brushFg: + for dc in dcList: + dc.SetBrush(brushFg) + self._lastBrushFg = brushFg + if self._lastPen != pen: + for dc in dcList: + dc.SetPen(pen) + self._lastPen = pen # }}} # {{{ _xlatePoint(self, relMapPoint): XXX def _xlatePoint(self, relMapPoint): @@ -102,6 +120,7 @@ class MiRCARTCanvasBackend(): def getDeviceContexts(self, parentWindow): eventDc = wx.ClientDC(parentWindow); tmpDc = wx.MemoryDC(); tmpDc.SelectObject(self.canvasBitmap) + self._lastBrushBg = self._lastBrushFg = self._lastPen = None; return (eventDc, tmpDc) # }}} # {{{ reset(self, canvasSize, cellSize): From ed8ccab26f3eb8df817ce139d7d8cea978f0fdb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Tue, 9 Jan 2018 22:23:27 +0100 Subject: [PATCH 093/148] MiRCART{Canvas{,Backend},Frame}.py: replace MemoryDC() w/ BufferedDC(canvasBitmap). --- MiRCARTCanvas.py | 37 ++++++++++----------- MiRCARTCanvasBackend.py | 72 ++++++++++++++++++++--------------------- MiRCARTFrame.py | 4 +-- 3 files changed, 55 insertions(+), 58 deletions(-) diff --git a/MiRCARTCanvas.py b/MiRCARTCanvas.py index b2e0021..c1855a3 100644 --- a/MiRCARTCanvas.py +++ b/MiRCARTCanvas.py @@ -39,15 +39,15 @@ class MiRCARTCanvas(wx.Panel): def _commitPatch(self, patch): self.canvasMap[patch[0][1]][patch[0][0]] = patch[1:] # }}} - # {{{ _dispatchInput(self, eventDc, patches, tmpDc): XXX - def _dispatchInput(self, eventDc, patches, tmpDc): + # {{{ _dispatchInput(self, eventDc, patches): XXX + def _dispatchInput(self, eventDc, patches): self.canvasBackend.drawCursorMaskWithJournal( \ - self.canvasJournal, eventDc, tmpDc) + self.canvasJournal, eventDc) cursorPatches = []; undoPatches = []; newPatches = []; for patchDescr in patches: patchIsCursor = patchDescr[0] for patch in patchDescr[1]: - if self.canvasBackend.drawPatch(eventDc, patch, tmpDc) == False: + if self.canvasBackend.drawPatch(eventDc, patch) == False: continue else: patchDeltaCell = self.canvasMap[patch[0][1]][patch[0][0]] @@ -69,7 +69,7 @@ class MiRCARTCanvas(wx.Panel): # }}} # {{{ onPanelKeyboardInput(self, event): XXX def onPanelKeyboardInput(self, event): - eventDc, tmpDc = self.canvasBackend.getDeviceContexts(self) + eventDc = self.canvasBackend.getDeviceContext(self) tool = self.canvasCurTool; mapPoint = self.brushPos; keyModifiers = event.GetModifiers() if keyModifiers != wx.MOD_NONE \ @@ -80,12 +80,12 @@ class MiRCARTCanvas(wx.Panel): event, mapPoint, self.brushColours, self.brushSize, \ chr(event.GetUnicodeKey())) if len(patches): - self._dispatchInput(eventDc, patches, tmpDc) + self._dispatchInput(eventDc, patches) self.parentFrame.onCanvasUpdate() # }}} # {{{ onPanelMouseInput(self, event): XXX def onPanelMouseInput(self, event): - eventDc, tmpDc = self.canvasBackend.getDeviceContexts(self) + eventDc = self.canvasBackend.getDeviceContext(self) tool = self.canvasCurTool self.brushPos = mapPoint = \ self.canvasBackend.xlateEventPoint(event, eventDc) @@ -93,23 +93,21 @@ class MiRCARTCanvas(wx.Panel): event, mapPoint, self.brushColours, self.brushSize, \ event.Dragging(), event.LeftIsDown(), event.RightIsDown()) if len(patches): - self._dispatchInput(eventDc, patches, tmpDc) + self._dispatchInput(eventDc, patches) self.parentFrame.onCanvasUpdate() self.parentFrame.onCanvasMotion(event, mapPoint) # }}} # {{{ onPanelFocus(self, event): XXX def onPanelFocus(self, event): if event.GetEventType() == wx.wxEVT_LEAVE_WINDOW: - eventDc, tmpDc = \ - self.canvasBackend.getDeviceContexts(self) + eventDc = self.canvasBackend.getDeviceContext(self) self.canvasBackend.drawCursorMaskWithJournal( \ - self.canvasJournal, eventDc, tmpDc) + self.canvasJournal, eventDc) self.parentFrame.onCanvasMotion(event) # }}} # {{{ onPanelPaint(self, event): XXX def onPanelPaint(self, event): - if self.canvasBackend.canvasBitmap != None: - eventDc = wx.BufferedPaintDC(self, self.canvasBackend.canvasBitmap) + self.canvasBackend.onPanelPaintEvent(self, event) # }}} # {{{ onStoreUpdate(self, newCanvasSize, newCanvas=None): def onStoreUpdate(self, newCanvasSize, newCanvas=None): @@ -119,7 +117,7 @@ class MiRCARTCanvas(wx.Panel): self.canvasMap = [[[(1, 1), 0, " "] \ for x in range(self.canvasSize[0])] \ for y in range(self.canvasSize[1])] - eventDc, tmpDc = self.canvasBackend.getDeviceContexts(self) + eventDc = self.canvasBackend.getDeviceContext(self) for numRow in range(self.canvasSize[1]): for numCol in range(self.canvasSize[0]): if newCanvas != None \ @@ -129,15 +127,15 @@ class MiRCARTCanvas(wx.Panel): [numCol, numRow], *newCanvas[numRow][numCol]]) self.canvasBackend.drawPatch(eventDc, \ ([numCol, numRow], \ - *self.canvasMap[numRow][numCol]), tmpDc) + *self.canvasMap[numRow][numCol])) wx.SafeYield() # }}} # {{{ popRedo(self): def popRedo(self): - eventDc, tmpDc = self.canvasBackend.getDeviceContexts(self) + eventDc = self.canvasBackend.getDeviceContext(self) patches = self.canvasJournal.popRedo() for patch in patches: - if self.canvasBackend.drawPatch(eventDc, patch, tmpDc) == False: + if self.canvasBackend.drawPatch(eventDc, patch) == False: continue else: self._commitPatch(patch) @@ -145,10 +143,10 @@ class MiRCARTCanvas(wx.Panel): # }}} # {{{ popUndo(self): def popUndo(self): - eventDc, tmpDc = self.canvasBackend.getDeviceContexts(self) + eventDc = self.canvasBackend.getDeviceContext(self) patches = self.canvasJournal.popUndo() for patch in patches: - if self.canvasBackend.drawPatch(eventDc, patch, tmpDc) == False: + if self.canvasBackend.drawPatch(eventDc, patch) == False: continue else: self._commitPatch(patch) @@ -178,7 +176,6 @@ class MiRCARTCanvas(wx.Panel): def __init__(self, parent, parentFrame, canvasPos, canvasSize, cellSize): super().__init__(parent, pos=canvasPos, \ size=[w*h for w,h in zip(canvasSize, cellSize)]) - self.SetDoubleBuffered(True) self.parentFrame = parentFrame self.canvasMap = None; self.canvasPos = canvasPos; self.canvasSize = canvasSize; diff --git a/MiRCARTCanvasBackend.py b/MiRCARTCanvasBackend.py index 34459db..192cc13 100644 --- a/MiRCARTCanvasBackend.py +++ b/MiRCARTCanvasBackend.py @@ -31,32 +31,30 @@ class MiRCARTCanvasBackend(): _lastBrush = _lastPen = None canvasBitmap = cellSize = None - # {{{ _drawBrushPatch(self, eventDc, patch, tmpDc): XXX - def _drawBrushPatch(self, eventDc, patch, tmpDc): + # {{{ _drawBrushPatch(self, eventDc, patch): XXX + def _drawBrushPatch(self, eventDc, patch): absPoint = self._xlatePoint(patch[0]) brushFg = self._brushes[patch[1][1]] brushBg = self._brushes[patch[1][0]] pen = self._pens[patch[1][1]] - self._setBrushDc(brushBg, brushFg, (eventDc, tmpDc), pen) + self._setBrushDc(brushBg, brushFg, eventDc, pen) eventDc.DrawRectangle(*absPoint, *self.cellSize) - tmpDc.DrawRectangle(*absPoint, *self.cellSize) # }}} - # {{{ _drawCharPatch(self, eventDc, patch, tmpDc): XXX - def _drawCharPatch(self, eventDc, patch, tmpDc): + # {{{ _drawCharPatch(self, eventDc, patch): XXX + def _drawCharPatch(self, eventDc, patch): absPoint = self._xlatePoint(patch[0]) brushFg = self._brushes[patch[1][0]] brushBg = self._brushes[patch[1][1]] pen = self._pens[patch[1][1]] - for dc in (eventDc, tmpDc): - fontBitmap = wx.Bitmap(*self.cellSize) - fontDc = wx.MemoryDC(); fontDc.SelectObject(fontBitmap); - fontDc.SetTextForeground(wx.Colour(MiRCARTColours[patch[1][0]][0:4])) - fontDc.SetTextBackground(wx.Colour(MiRCARTColours[patch[1][1]][0:4])) - fontDc.SetBrush(brushBg); fontDc.SetBackground(brushBg); fontDc.SetPen(pen); - fontDc.SetFont(self._font) - fontDc.DrawRectangle(0, 0, *self.cellSize) - fontDc.DrawText(patch[3], 0, 0) - dc.Blit(*absPoint, *self.cellSize, fontDc, 0, 0) + fontBitmap = wx.Bitmap(*self.cellSize) + fontDc = wx.MemoryDC(); fontDc.SelectObject(fontBitmap); + fontDc.SetTextForeground(wx.Colour(MiRCARTColours[patch[1][0]][0:4])) + fontDc.SetTextBackground(wx.Colour(MiRCARTColours[patch[1][1]][0:4])) + fontDc.SetBrush(brushBg); fontDc.SetBackground(brushBg); fontDc.SetPen(pen); + fontDc.SetFont(self._font) + fontDc.DrawRectangle(0, 0, *self.cellSize) + fontDc.DrawText(patch[3], 0, 0) + eventDc.Blit(*absPoint, *self.cellSize, fontDc, 0, 0) # }}} # {{{ _finiBrushesAndPens(self): XXX def _finiBrushesAndPens(self): @@ -79,19 +77,16 @@ class MiRCARTCanvasBackend(): wx.Colour(MiRCARTColours[mircColour][0:4]), 1) self._lastBrushBg = self._lastBrushFg = self._lastPen = None; # }}} - # {{{ _setBrushDc(self, brushBg, brushFg, dcList, pen): XXX - def _setBrushDc(self, brushBg, brushFg, dcList, pen): + # {{{ _setBrushDc(self, brushBg, brushFg, dc, pen): XXX + def _setBrushDc(self, brushBg, brushFg, dc, pen): if self._lastBrushBg != brushBg: - for dc in dcList: - dc.SetBackground(brushBg) + dc.SetBackground(brushBg) self._lastBrushBg = brushBg if self._lastBrushFg != brushFg: - for dc in dcList: - dc.SetBrush(brushFg) + dc.SetBrush(brushFg) self._lastBrushFg = brushFg if self._lastPen != pen: - for dc in dcList: - dc.SetPen(pen) + dc.SetPen(pen) self._lastPen = pen # }}} # {{{ _xlatePoint(self, relMapPoint): XXX @@ -99,29 +94,34 @@ class MiRCARTCanvasBackend(): return [a*b for a,b in zip(relMapPoint, self.cellSize)] # }}} - # {{{ drawPatch(self, eventDc, patch, tmpDc): XXX - def drawPatch(self, eventDc, patch, tmpDc): + # {{{ drawPatch(self, eventDc, patch): XXX + def drawPatch(self, eventDc, patch): if patch[0][0] < self.canvasSize[0] \ and patch[0][1] < self.canvasSize[1]: if patch[3] == " ": - self._drawBrushPatch(eventDc, patch, tmpDc) + self._drawBrushPatch(eventDc, patch) else: - self._drawCharPatch(eventDc, patch, tmpDc) + self._drawCharPatch(eventDc, patch) return True else: return False # }}} - # {{{ drawCursorMaskWithJournal(self, canvasJournal, eventDc, tmpDc): XXX - def drawCursorMaskWithJournal(self, canvasJournal, eventDc, tmpDc): + # {{{ drawCursorMaskWithJournal(self, canvasJournal, eventDc): XXX + def drawCursorMaskWithJournal(self, canvasJournal, eventDc): for patch in canvasJournal.popCursor(): - self.drawPatch(eventDc, patch, tmpDc) + self.drawPatch(eventDc, patch) # }}} - # {{{ getDeviceContexts(self, parentWindow): XXX - def getDeviceContexts(self, parentWindow): - eventDc = wx.ClientDC(parentWindow); tmpDc = wx.MemoryDC(); - tmpDc.SelectObject(self.canvasBitmap) + # {{{ getDeviceContext(self, parentWindow): XXX + def getDeviceContext(self, parentWindow): + eventDc = wx.BufferedDC( \ + wx.ClientDC(parentWindow), self.canvasBitmap) self._lastBrushBg = self._lastBrushFg = self._lastPen = None; - return (eventDc, tmpDc) + return eventDc + # }}} + # {{{ onPanelPaintEvent(self, panelWindow, panelEvent): XXX + def onPanelPaintEvent(self, panelWindow, panelEvent): + if self.canvasBitmap != None: + eventDc = wx.BufferedPaintDC(panelWindow, self.canvasBitmap) # }}} # {{{ reset(self, canvasSize, cellSize): def reset(self, canvasSize, cellSize): diff --git a/MiRCARTFrame.py b/MiRCARTFrame.py index b7e5e5d..68d1403 100644 --- a/MiRCARTFrame.py +++ b/MiRCARTFrame.py @@ -358,7 +358,7 @@ class MiRCARTFrame(MiRCARTGeneralFrame): pass elif cid == self.CID_INCR_CANVAS[0] \ or cid == self.CID_DECR_CANVAS[0]: - eventDc, tmpDc = self.panelCanvas.canvasBackend.getDeviceContexts(self) + eventDc = self.panelCanvas.canvasBackend.getDeviceContext(self) if cid == self.CID_INCR_CANVAS[0]: newCanvasSize = [a+1 for a in self.panelCanvas.canvasSize] else: @@ -373,7 +373,7 @@ class MiRCARTFrame(MiRCARTGeneralFrame): for numCol in range(self.panelCanvas.canvasSize[0]): self.panelCanvas.canvasMap[-1].append([[1, 1], 0, " "]) self.panelCanvas.canvasBackend.drawPatch(eventDc, \ - ([numCol, self.panelCanvas.canvasSize[1] - 1], *[[1, 1], 0, " "]), tmpDc) + ([numCol, self.panelCanvas.canvasSize[1] - 1], *[[1, 1], 0, " "])) wx.SafeYield() elif cid == self.CID_INCR_BRUSH[0]: self.panelCanvas.brushSize = \ From cd5ea1fab5d7dad7e9d2c8170098fb7510b0f3e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Wed, 10 Jan 2018 00:25:42 +0100 Subject: [PATCH 094/148] MiRCARTCanvas{,Journal,Store}.py: dispatch patches from tool event handlers directly. MiRCARTTool{,Circle,Line,Rect,Text}.py: updated to new interface. --- MiRCARTCanvas.py | 173 ++++++++++++++++++---------------------- MiRCARTCanvasJournal.py | 11 ++- MiRCARTCanvasStore.py | 5 +- MiRCARTTool.py | 10 +-- MiRCARTToolCircle.py | 17 ++-- MiRCARTToolLine.py | 29 ++++--- MiRCARTToolRect.py | 18 ++--- MiRCARTToolText.py | 24 +++--- 8 files changed, 138 insertions(+), 149 deletions(-) diff --git a/MiRCARTCanvas.py b/MiRCARTCanvas.py index c1855a3..ceef200 100644 --- a/MiRCARTCanvas.py +++ b/MiRCARTCanvas.py @@ -35,88 +35,79 @@ class MiRCARTCanvas(wx.Panel): brushColours = brushPos = brushSize = None canvasBackend = canvasCurTool = canvasJournal = canvasStore = None - # {{{ _commitPatch(self, patch): + # {{{ _commitPatch(self, patch): XXX def _commitPatch(self, patch): self.canvasMap[patch[0][1]][patch[0][0]] = patch[1:] # }}} - # {{{ _dispatchInput(self, eventDc, patches): XXX - def _dispatchInput(self, eventDc, patches): - self.canvasBackend.drawCursorMaskWithJournal( \ - self.canvasJournal, eventDc) - cursorPatches = []; undoPatches = []; newPatches = []; - for patchDescr in patches: - patchIsCursor = patchDescr[0] - for patch in patchDescr[1]: - if self.canvasBackend.drawPatch(eventDc, patch) == False: - continue - else: - patchDeltaCell = self.canvasMap[patch[0][1]][patch[0][0]] - if patchIsCursor == True: - cursorPatches.append([list(patch[0]), *patchDeltaCell.copy()]) - else: - undoPatches.append([list(patch[0]), *patchDeltaCell.copy()]) - newPatches.append(patch) - self._commitPatch(patch) - if len(cursorPatches): - self.canvasJournal.pushCursor(cursorPatches) - if len(undoPatches): - self.canvasJournal.pushDeltas(undoPatches, newPatches) + # {{{ _dispatchDeltaPatches(self, deltaPatches): XXX + def _dispatchDeltaPatches(self, deltaPatches): + eventDc = self.canvasBackend.getDeviceContext(self) + for patch in deltaPatches: + if self.canvasBackend.drawPatch(eventDc, patch): + self._commitPatch(patch) + self.parentFrame.onCanvasUpdate() + # }}} + # {{{ _dispatchPatch(self, eventDc, isCursor, patch): XXX + def _dispatchPatch(self, eventDc, isCursor, patch): + if not self._canvasDirtyCursor: + self.canvasBackend.drawCursorMaskWithJournal( \ + self.canvasJournal, eventDc) + self._canvasDirtyCursor = True + if self.canvasBackend.drawPatch(eventDc, patch): + patchDeltaCell = self.canvasMap[patch[0][1]][patch[0][0]] + patchDelta = [list(patch[0]), *patchDeltaCell.copy()] + if isCursor: + self.canvasJournal.pushCursor(patchDelta) + else: + if not self._canvasDirty: + self.canvasJournal.pushDeltas([], []) + self._canvasDirty = True + self.canvasJournal.updateCurrentDeltas(patchDelta, patch) + self._commitPatch(patch) # }}} # {{{ onPanelClose(self, event): XXX def onPanelClose(self, event): self.Destroy() # }}} - # {{{ onPanelKeyboardInput(self, event): XXX - def onPanelKeyboardInput(self, event): - eventDc = self.canvasBackend.getDeviceContext(self) - tool = self.canvasCurTool; mapPoint = self.brushPos; - keyModifiers = event.GetModifiers() - if keyModifiers != wx.MOD_NONE \ - and keyModifiers != wx.MOD_SHIFT: - event.Skip() - else: - patches = tool.onKeyboardEvent( \ - event, mapPoint, self.brushColours, self.brushSize, \ - chr(event.GetUnicodeKey())) - if len(patches): - self._dispatchInput(eventDc, patches) - self.parentFrame.onCanvasUpdate() - # }}} - # {{{ onPanelMouseInput(self, event): XXX - def onPanelMouseInput(self, event): + # {{{ onPanelInput(self, event): XXX + def onPanelInput(self, event): eventDc = self.canvasBackend.getDeviceContext(self) + eventType = event.GetEventType() + self._canvasDirty = self._canvasDirtyCursor = False tool = self.canvasCurTool - self.brushPos = mapPoint = \ - self.canvasBackend.xlateEventPoint(event, eventDc) - patches = tool.onMouseEvent( \ - event, mapPoint, self.brushColours, self.brushSize, \ - event.Dragging(), event.LeftIsDown(), event.RightIsDown()) - if len(patches): - self._dispatchInput(eventDc, patches) + if eventType == wx.wxEVT_CHAR: + mapPoint = self.brushPos + doSkip = tool.onKeyboardEvent( \ + event, mapPoint, self.brushColours, self.brushSize, \ + chr(event.GetUnicodeKey()), self._dispatchPatch, eventDc) + if doSkip: + event.Skip(); return; + else: + mapPoint = self.canvasBackend.xlateEventPoint(event, eventDc) + self.brushPos = mapPoint + tool.onMouseEvent( \ + event, mapPoint, self.brushColours, self.brushSize, \ + event.Dragging(), event.LeftIsDown(), event.RightIsDown(), \ + self._dispatchPatch, eventDc) + if self._canvasDirty: self.parentFrame.onCanvasUpdate() self.parentFrame.onCanvasMotion(event, mapPoint) # }}} - # {{{ onPanelFocus(self, event): XXX - def onPanelFocus(self, event): - if event.GetEventType() == wx.wxEVT_LEAVE_WINDOW: - eventDc = self.canvasBackend.getDeviceContext(self) - self.canvasBackend.drawCursorMaskWithJournal( \ - self.canvasJournal, eventDc) + # {{{ onPanelLeaveWindow(self, event): XXX + def onPanelLeaveWindow(self, event): + eventDc = self.canvasBackend.getDeviceContext(self) + self.canvasBackend.drawCursorMaskWithJournal( \ + self.canvasJournal, eventDc) self.parentFrame.onCanvasMotion(event) # }}} # {{{ onPanelPaint(self, event): XXX def onPanelPaint(self, event): self.canvasBackend.onPanelPaintEvent(self, event) # }}} - # {{{ onStoreUpdate(self, newCanvasSize, newCanvas=None): + # {{{ onStoreUpdate(self, newCanvasSize, newCanvas=None): XXX def onStoreUpdate(self, newCanvasSize, newCanvas=None): self.resize(newCanvasSize=newCanvasSize) - self.canvasBackend.reset(self.canvasSize, self.canvasBackend.cellSize) - self.canvasJournal.resetCursor(); self.canvasJournal.resetUndo(); - self.canvasMap = [[[(1, 1), 0, " "] \ - for x in range(self.canvasSize[0])] \ - for y in range(self.canvasSize[1])] eventDc = self.canvasBackend.getDeviceContext(self) for numRow in range(self.canvasSize[1]): for numCol in range(self.canvasSize[0]): @@ -130,45 +121,36 @@ class MiRCARTCanvas(wx.Panel): *self.canvasMap[numRow][numCol])) wx.SafeYield() # }}} - # {{{ popRedo(self): + # {{{ popRedo(self): XXX def popRedo(self): - eventDc = self.canvasBackend.getDeviceContext(self) - patches = self.canvasJournal.popRedo() - for patch in patches: - if self.canvasBackend.drawPatch(eventDc, patch) == False: - continue - else: - self._commitPatch(patch) - self.parentFrame.onCanvasUpdate() + self._dispatchDeltaPatches(self.canvasJournal.popRedo()) # }}} - # {{{ popUndo(self): + # {{{ popUndo(self): XXX def popUndo(self): - eventDc = self.canvasBackend.getDeviceContext(self) - patches = self.canvasJournal.popUndo() - for patch in patches: - if self.canvasBackend.drawPatch(eventDc, patch) == False: - continue - else: - self._commitPatch(patch) - self.parentFrame.onCanvasUpdate() + self._dispatchDeltaPatches(self.canvasJournal.popUndo()) # }}} - # {{{ resize(self, newCanvasSize): + # {{{ resize(self, newCanvasSize): XXX def resize(self, newCanvasSize): if newCanvasSize != self.canvasSize: - winSize = [a*b for a,b in \ - zip(newCanvasSize, self.canvasBackend.cellSize)] - self.SetSize(*self.canvasPos, *winSize) - for numRow in range(self.canvasSize[1]): - for numNewCol in range(self.canvasSize[0], newCanvasSize[0]): - self.canvasMap[numRow].append([[1, 1], 0, " "]) - for numNewRow in range(self.canvasSize[1], newCanvasSize[1]): - self.canvasMap.append([]) - for numNewCol in range(newCanvasSize[0]): - self.canvasMap[numNewRow].append([[1, 1], 0, " "]) + if self.canvasMap == None: + self.canvasMap = [[[(1, 1), 0, " "] \ + for x in range(self.canvasSize[0])] \ + for y in range(self.canvasSize[1])] + else: + for numRow in range(self.canvasSize[1]): + for numNewCol in range(self.canvasSize[0], newCanvasSize[0]): + self.canvasMap[numRow].append([[1, 1], 0, " "]) + for numNewRow in range(self.canvasSize[1], newCanvasSize[1]): + self.canvasMap.append([]) + for numNewCol in range(newCanvasSize[0]): + self.canvasMap[numNewRow].append([[1, 1], 0, " "]) self.canvasSize = newCanvasSize - self.canvasBackend.reset(self.canvasSize, \ - self.canvasBackend.cellSize) - self.parentFrame.onCanvasUpdate() + self.SetSize(*self.canvasPos, \ + *[a*b for a,b in zip(self.canvasSize, \ + self.canvasBackend.cellSize)]) + self.canvasBackend.reset(self.canvasSize, self.canvasBackend.cellSize) + self.canvasJournal.resetCursor(); self.canvasJournal.resetUndo(); + self.parentFrame.onCanvasUpdate() # }}} # @@ -187,12 +169,11 @@ class MiRCARTCanvas(wx.Panel): # Bind event handlers self.Bind(wx.EVT_CLOSE, self.onPanelClose) - self.Bind(wx.EVT_ENTER_WINDOW, self.onPanelFocus) - self.Bind(wx.EVT_LEAVE_WINDOW, self.onPanelFocus) - self.parentFrame.Bind(wx.EVT_CHAR, self.onPanelKeyboardInput) + self.Bind(wx.EVT_LEAVE_WINDOW, self.onPanelLeaveWindow) + self.parentFrame.Bind(wx.EVT_CHAR, self.onPanelInput) for eventType in( \ wx.EVT_LEFT_DOWN, wx.EVT_MOTION, wx.EVT_RIGHT_DOWN): - self.Bind(eventType, self.onPanelMouseInput) + self.Bind(eventType, self.onPanelInput) self.Bind(wx.EVT_PAINT, self.onPanelPaint) # vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/MiRCARTCanvasJournal.py b/MiRCARTCanvasJournal.py index 17706e7..d487a60 100644 --- a/MiRCARTCanvasJournal.py +++ b/MiRCARTCanvasJournal.py @@ -55,14 +55,16 @@ class MiRCARTCanvasJournal(): # }}} # {{{ pushCursor(self, patches): XXX def pushCursor(self, patches): - self.patchesCursor += patches + self.patchesCursor.append(patches) # }}} # {{{ pushDeltas(self, undoPatches, redoPatches): XXX def pushDeltas(self, undoPatches, redoPatches): if self.patchesUndoLevel > 0: del self.patchesUndo[0:self.patchesUndoLevel] self.patchesUndoLevel = 0 - self.patchesUndo.insert(0, [undoPatches, redoPatches]) + deltaItem = [undoPatches, redoPatches] + self.patchesUndo.insert(0, deltaItem) + return deltaItem # }}} # {{{ resetCursor(self): XXX def resetCursor(self): @@ -72,6 +74,11 @@ class MiRCARTCanvasJournal(): def resetUndo(self): self.patchesUndo = [None]; self.patchesUndoLevel = 0; # }}} + # {{{ updateCurrentDeltas(self, undoPatches, redoPatches): XXX + def updateCurrentDeltas(self, undoPatches, redoPatches): + self.patchesUndo[0][0].append(undoPatches) + self.patchesUndo[0][1].append(redoPatches) + # }}} # # __init__(self): initialisation method diff --git a/MiRCARTCanvasStore.py b/MiRCARTCanvasStore.py index e41b35a..79b609e 100644 --- a/MiRCARTCanvasStore.py +++ b/MiRCARTCanvasStore.py @@ -235,7 +235,10 @@ class MiRCARTCanvasStore(): # }}} # {{{ importNew(self, newCanvasSize=None): XXX def importNew(self, newCanvasSize=None): - self.parentCanvas.onStoreUpdate(newCanvasSize) + newMap = [[[(1, 1), 0, " "] \ + for x in range(self.parentCanvas.canvasSize[0])] \ + for y in range(self.parentCanvas.canvasSize[1])] + self.parentCanvas.onStoreUpdate(newCanvasSize, newMap) # }}} # diff --git a/MiRCARTTool.py b/MiRCARTTool.py index be8758c..4798ebf 100644 --- a/MiRCARTTool.py +++ b/MiRCARTTool.py @@ -26,12 +26,12 @@ class MiRCARTTool(): """XXX""" parentCanvas = None - # {{{ onKeyboardEvent(self, event, atPoint, brushColours, brushSize, keyChar): - def onKeyboardEvent(self, event, atPoint, brushColours, brushSize, keyChar): - return () + # {{{ onKeyboardEvent(self, event, atPoint, brushColours, brushSize, keyChar, dispatchFn, eventDc): + def onKeyboardEvent(self, event, atPoint, brushColours, brushSize, keyChar, dispatchFn, eventDc): + return True # }}} - # {{{ onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown): XXX - def onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown): + # {{{ onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown, dispatchFn, eventDc): XXX + def onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown, dispatchFn, eventDc): return () # }}} diff --git a/MiRCARTToolCircle.py b/MiRCARTToolCircle.py index 8218e85..52497e3 100644 --- a/MiRCARTToolCircle.py +++ b/MiRCARTToolCircle.py @@ -28,8 +28,8 @@ class MiRCARTToolCircle(MiRCARTTool): """XXX""" # - # onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown): XXX - def onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown): + # onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown, dispatchFn, eventDc): XXX + def onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown, dispatchFn, eventDc): brushColours = brushColours.copy() if isLeftDown: brushColours[1] = brushColours[0] @@ -37,20 +37,19 @@ class MiRCARTToolCircle(MiRCARTTool): brushColours[0] = brushColours[1] else: brushColours[1] = brushColours[0] - brushPatches = [] _brushSize = brushSize[0]*2 originPoint = (_brushSize/2, _brushSize/2) radius = _brushSize for brushY in range(-radius, radius + 1): for brushX in range(-radius, radius + 1): if ((brushX**2)+(brushY**2) < (((radius**2)+radius)*0.8)): - brushPatches.append([ \ + patch = [ \ [atPoint[0] + int(originPoint[0]+brushX), \ atPoint[1] + int(originPoint[1]+brushY)], \ - brushColours, 0, " "]) - if isLeftDown or isRightDown: - return [[False, brushPatches], [True, brushPatches]] - else: - return [[True, brushPatches]] + brushColours, 0, " "] + if isLeftDown or isRightDown: + dispatchFn(eventDc, False, patch); dispatchFn(eventDc, True, patch); + else: + dispatchFn(eventDc, True, patch) # vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/MiRCARTToolLine.py b/MiRCARTToolLine.py index 18652d3..3180d0d 100644 --- a/MiRCARTToolLine.py +++ b/MiRCARTToolLine.py @@ -39,8 +39,8 @@ class MiRCARTToolLine(MiRCARTTool): def _pointSwap(self, a, b): return [b, a] # }}} - # {{{ _getLine(self, brushColours, originPoint, targetPoint): XXX - def _getLine(self, brushColours, originPoint, targetPoint): + # {{{ _getLine(self, brushColours, eventDc, isCursor, originPoint, targetPoint, dispatchFn): XXX + def _getLine(self, brushColours, eventDc, isCursor, originPoint, targetPoint, dispatchFn): originPoint = originPoint.copy(); targetPoint = targetPoint.copy(); pointDelta = self._pointDelta(originPoint, targetPoint) lineXSign = 1 if pointDelta[0] > 0 else -1; @@ -52,21 +52,23 @@ class MiRCARTToolLine(MiRCARTTool): pointDelta = [pointDelta[1], pointDelta[0]] lineXX, lineXY, lineYX, lineYY = 0, lineYSign, lineXSign, 0 lineD = 2 * pointDelta[1] - pointDelta[0]; lineY = 0; - linePatches = [] for lineX in range(pointDelta[0] + 1): - linePatches.append([[ \ + patch = [[ \ originPoint[0] + lineX*lineXX + lineY*lineYX, \ originPoint[1] + lineX*lineXY + lineY*lineYY], \ - brushColours, 0, " "]) + brushColours, 0, " "] + if isCursor: + dispatchFn(eventDc, False, patch); dispatchFn(eventDc, True, patch); + else: + dispatchFn(eventDc, True, patch) if lineD > 0: lineD -= pointDelta[0]; lineY += 1; lineD += pointDelta[1] - return linePatches # }}} # - # onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown): XXX - def onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown): + # onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown, dispatchFn, eventDc): XXX + def onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown, dispatchFn, eventDc): brushColours = brushColours.copy() if isLeftDown: brushColours[1] = brushColours[0] @@ -74,22 +76,19 @@ class MiRCARTToolLine(MiRCARTTool): brushColours[0] = brushColours[1] else: brushColours[1] = brushColours[0] - brushPatches = []; tmpPatches = []; if self.toolState == self.TS_NONE: if isLeftDown or isRightDown: self.toolOriginPoint = list(atPoint) self.toolState = self.TS_ORIGIN - tmpPatches.append([atPoint, brushColours, 0, " "]) - return [[True, tmpPatches]] + dispatchFn(eventDc, True, [atPoint, brushColours, 0, " "]) elif self.toolState == self.TS_ORIGIN: targetPoint = list(atPoint) originPoint = self.toolOriginPoint - brushPatches = self._getLine(brushColours, originPoint, targetPoint) + self._getLine(brushColours, eventDc, \ + isLeftDown or isRightDown, \ + originPoint, targetPoint, dispatchFn) if isLeftDown or isRightDown: self.toolState = self.TS_NONE - return [[False, brushPatches], [True, brushPatches]] - else: - return [[True, brushPatches]] # __init__(self, *args): initialisation method def __init__(self, *args): diff --git a/MiRCARTToolRect.py b/MiRCARTToolRect.py index a4e9662..a1455a3 100644 --- a/MiRCARTToolRect.py +++ b/MiRCARTToolRect.py @@ -28,8 +28,8 @@ class MiRCARTToolRect(MiRCARTTool): """XXX""" # - # onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown): XXX - def onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown): + # onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown, dispatchFn, eventDc): XXX + def onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown, dispatchFn, eventDc): brushColours = brushColours.copy() if isLeftDown: brushColours[1] = brushColours[0] @@ -40,16 +40,12 @@ class MiRCARTToolRect(MiRCARTTool): brushSize = brushSize.copy() if brushSize[0] > 1: brushSize[0] *= 2 - brushPatches = [] for brushRow in range(brushSize[1]): for brushCol in range(brushSize[0]): - brushPatches.append([[ \ - atPoint[0] + brushCol, \ - atPoint[1] + brushRow], \ - brushColours, 0, " "]) - if isLeftDown or isRightDown: - return [[False, brushPatches], [True, brushPatches]] - else: - return [[True, brushPatches]] + patch = [[atPoint[0] + brushCol, atPoint[1] + brushRow],brushColours, 0, " "] + if isLeftDown or isRightDown: + dispatchFn(eventDc, False, patch); dispatchFn(eventDc, True, patch); + else: + dispatchFn(eventDc, True, patch) # vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/MiRCARTToolText.py b/MiRCARTToolText.py index 8f1be6d..ca1386a 100644 --- a/MiRCARTToolText.py +++ b/MiRCARTToolText.py @@ -23,23 +23,27 @@ # from MiRCARTTool import MiRCARTTool -import string +import string, wx class MiRCARTToolText(MiRCARTTool): """XXX""" textColours = textPos = None # - # onKeyboardEvent(self, event, atPoint, brushColours, brushSize, keyChar): XXX - def onKeyboardEvent(self, event, atPoint, brushColours, brushSize, keyChar): - if not keyChar in string.printable: - return [] + # onKeyboardEvent(self, event, atPoint, brushColours, brushSize, keyChar, dispatchFn, eventDc): XXX + def onKeyboardEvent(self, event, atPoint, brushColours, brushSize, keyChar, dispatchFn, eventDc): + keyModifiers = event.GetModifiers() + if keyModifiers != wx.MOD_NONE \ + and keyModifiers != wx.MOD_SHIFT: + return True + elif not keyChar in string.printable: + return True else: if self.textColours == None: self.textColours = brushColours.copy() if self.textPos == None: self.textPos = list(atPoint) - patches = [[False, [[self.textPos, self.textColours, 0, keyChar]]]] + dispatchFn(eventDc, False, [self.textPos, self.textColours, 0, keyChar]) if self.textPos[0] < (self.parentCanvas.canvasSize[0] - 1): self.textPos[0] += 1 elif self.textPos[1] < (self.parentCanvas.canvasSize[1] - 1): @@ -47,11 +51,11 @@ class MiRCARTToolText(MiRCARTTool): self.textPos[1] += 1 else: self.textPos = [0, 0] - return patches + return False # - # onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown): XXX - def onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown): + # onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown, dispatchFn, eventDc): XXX + def onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown, dispatchFn, eventDc): if isLeftDown: self.textColours = brushColours.copy() self.textPos = list(atPoint) @@ -62,6 +66,6 @@ class MiRCARTToolText(MiRCARTTool): if self.textColours == None: self.textColours = brushColours.copy() self.textPos = list(atPoint) - return [[True, [[self.textPos, self.textColours, 0, "_"]]]] + dispatchFn(eventDc, True, [self.textPos, self.textColours, 0, "_"]) # vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 From 398ee3af3d5e4f757087a6ca510f8bfff4300aec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Wed, 10 Jan 2018 00:48:13 +0100 Subject: [PATCH 095/148] MiRCARTCanvas{Export,Import}Store.py: split from MiRCARTCanvasStore.py. IrcMiRCARTBot.py, MiRCART{Canvas,Frame,ToPngFile}.py: updated. --- IrcMiRCARTBot.py | 4 +- MiRCARTCanvas.py | 9 +- MiRCARTCanvasExportStore.py | 128 ++++++++++++++++++ ...vasStore.py => MiRCARTCanvasImportStore.py | 102 +------------- MiRCARTFrame.py | 16 +-- MiRCARTToPngFile.py | 4 +- 6 files changed, 148 insertions(+), 115 deletions(-) create mode 100644 MiRCARTCanvasExportStore.py rename MiRCARTCanvasStore.py => MiRCARTCanvasImportStore.py (60%) diff --git a/IrcMiRCARTBot.py b/IrcMiRCARTBot.py index b943f23..9977488 100755 --- a/IrcMiRCARTBot.py +++ b/IrcMiRCARTBot.py @@ -27,7 +27,7 @@ import os, sys, time import json import IrcClient import requests, urllib.request -from MiRCARTCanvasStore import MiRCARTCanvasStore +from MiRCARTCanvasImportStore import MiRCARTCanvasImportStore from MiRCARTToPngFile import MiRCARTToPngFile class IrcMiRCARTBot(IrcClient.IrcClient): @@ -141,7 +141,7 @@ class IrcMiRCARTBot(IrcClient.IrcClient): self.queue("PRIVMSG", message[2], "4/!\\ Unknown URL type specified!") return - canvasStore = MiRCARTCanvasStore(inFile=asciiTmpFilePath) + canvasStore = MiRCARTCanvasImportStore(inFile=asciiTmpFilePath) MiRCARTToPngFile(canvasStore.outMap, "DejaVuSansMono.ttf", 11).export(imgTmpFilePath) imgurResponse = self._uploadToImgur(imgTmpFilePath, "MiRCART image", "MiRCART image", "c9a6efb3d7932fd") if imgurResponse[0] == 200: diff --git a/MiRCARTCanvas.py b/MiRCARTCanvas.py index ceef200..0ad4ff2 100644 --- a/MiRCARTCanvas.py +++ b/MiRCARTCanvas.py @@ -24,7 +24,8 @@ from MiRCARTCanvasBackend import MiRCARTCanvasBackend from MiRCARTCanvasJournal import MiRCARTCanvasJournal -from MiRCARTCanvasStore import MiRCARTCanvasStore, haveMiRCARTToPngFile, haveUrllib +from MiRCARTCanvasExportStore import MiRCARTCanvasExportStore, haveMiRCARTToPngFile, haveUrllib +from MiRCARTCanvasImportStore import MiRCARTCanvasImportStore from MiRCARTColours import MiRCARTColours import wx @@ -33,7 +34,8 @@ class MiRCARTCanvas(wx.Panel): parentFrame = None canvasMap = canvasPos = canvasSize = None brushColours = brushPos = brushSize = None - canvasBackend = canvasCurTool = canvasJournal = canvasStore = None + canvasBackend = canvasCurTool = canvasJournal = None + canvasExportStore = canvasImportStore = None # {{{ _commitPatch(self, patch): XXX def _commitPatch(self, patch): @@ -165,7 +167,8 @@ class MiRCARTCanvas(wx.Panel): self.canvasBackend = MiRCARTCanvasBackend(canvasSize, cellSize) self.canvasCurTool = None self.canvasJournal = MiRCARTCanvasJournal() - self.canvasStore = MiRCARTCanvasStore(parentCanvas=self) + self.canvasExportStore = MiRCARTCanvasExportStore(parentCanvas=self) + self.canvasImportStore = MiRCARTCanvasImportStore(parentCanvas=self) # Bind event handlers self.Bind(wx.EVT_CLOSE, self.onPanelClose) diff --git a/MiRCARTCanvasExportStore.py b/MiRCARTCanvasExportStore.py new file mode 100644 index 0000000..e7c520d --- /dev/null +++ b/MiRCARTCanvasExportStore.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +# +# MiRCARTCanvasExportStore.py -- XXX +# Copyright (c) 2018 Lucio Andrés Illanes Albornoz +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +import io, os, tempfile + +try: + from MiRCARTToPngFile import MiRCARTToPngFile + haveMiRCARTToPngFile = True +except ImportError: + haveMiRCARTToPngFile = False + +try: + import base64, json, requests, urllib.request + haveUrllib = True +except ImportError: + haveUrllib = False + +class MiRCARTCanvasExportStore(): + """XXX""" + parentCanvas = None + + # {{{ _exportFileToImgur(self, apiKey, imgName, imgTitle, pathName): upload single PNG file to Imgur + def _exportFileToImgur(self, apiKey, imgName, imgTitle, pathName): + requestImageData = open(pathName, "rb").read() + requestData = { \ + "image": base64.b64encode(requestImageData), \ + "key": apiKey, \ + "name": imgName, \ + "title": imgTitle, \ + "type": "base64"} + requestHeaders = {"Authorization": "Client-ID " + apiKey} + responseHttp = requests.post( \ + "https://api.imgur.com/3/upload.json", \ + data=requestData, headers=requestHeaders) + responseDict = json.loads(responseHttp.text) + if responseHttp.status_code == 200: + return [200, responseDict.get("data").get("link")] + else: + return [responseHttp.status_code, ""] + # }}} + + # {{{ exportBitmapToPngFile(self, canvasBitmap, outPathName, outType): XXX + def exportBitmapToPngFile(self, canvasBitmap, outPathName, outType): + return canvasBitmap.ConvertToImage().SaveFile(outPathName, outType) + # }}} + # {{{ exportBitmapToImgur(self, apiKey, canvasBitmap, imgName, imgTitle, imgType): XXX + def exportBitmapToImgur(self, apiKey, canvasBitmap, imgName, imgTitle, imgType): + tmpPathName = tempfile.mkstemp() + os.close(tmpPathName[0]) + canvasBitmap.ConvertToImage().SaveFile(tmpPathName[1], imgType) + imgurResult = self._exportFileToImgur(apiKey, imgName, imgTitle, tmpPathName[1]) + os.remove(tmpPathName[1]) + return imgurResult + # }}} + # {{{ exportPastebin(self, apiDevKey, canvasMap, canvasSize, pasteName="", pastePrivate=0): XXX + def exportPastebin(self, apiDevKey, canvasMap, canvasSize, pasteName="", pastePrivate=0): + if haveUrllib: + outFile = io.StringIO() + self.exportTextFile(canvasMap, canvasSize, outFile) + requestData = { \ + "api_dev_key": apiDevKey, \ + "api_option": "paste", \ + "api_paste_code": outFile.getvalue().encode(), \ + "api_paste_name": pasteName, \ + "api_paste_private": pastePrivate} + responseHttp = requests.post("https://pastebin.com/api/api_post.php", \ + data=requestData) + if responseHttp.status_code == 200: + if responseHttp.text.startswith("http"): + return (True, responseHttp.text) + else: + return (False, responseHttp.text) + else: + return (False, str(responseHttp.status_code)) + else: + return (False, "missing requests and/or urllib3 module(s)") + # }}} + # {{{ exportPngFile(self, canvasMap, outPathName): XXX + def exportPngFile(self, canvasMap, outPathName): + if haveMiRCARTToPngFile: + MiRCARTToPngFile(canvasMap).export(outPathName) + return True + else: + return False + # }}} + # {{{ exportTextFile(self, canvasMap, canvasSize, outFile): XXX + def exportTextFile(self, canvasMap, canvasSize, outFile): + for canvasRow in range(canvasSize[1]): + canvasLastColours = [] + for canvasCol in range(canvasSize[0]): + canvasColColours = canvasMap[canvasRow][canvasCol][0] + canvasColText = canvasMap[canvasRow][canvasCol][2] + if canvasColColours != canvasLastColours: + canvasLastColours = canvasColColours + outFile.write("\x03" + \ + str(canvasColColours[0]) + \ + "," + str(canvasColColours[1])) + outFile.write(canvasColText) + outFile.write("\n") + # }}} + + # + # __init__(self, parentCanvas): initialisation method + def __init__(self, parentCanvas): + self.parentCanvas = parentCanvas + +# vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/MiRCARTCanvasStore.py b/MiRCARTCanvasImportStore.py similarity index 60% rename from MiRCARTCanvasStore.py rename to MiRCARTCanvasImportStore.py index 79b609e..6bce3bf 100644 --- a/MiRCARTCanvasStore.py +++ b/MiRCARTCanvasImportStore.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# MiRCARTCanvasStore.py -- XXX +# MiRCARTCanvasImportStore.py -- XXX # Copyright (c) 2018 Lucio Andrés Illanes Albornoz # # Permission is hereby granted, free of charge, to any person obtaining a copy @@ -22,27 +22,7 @@ # SOFTWARE. # -import io, os, tempfile - -try: - import wx - haveWx = True -except ImportError: - haveWx = False - -try: - from MiRCARTToPngFile import MiRCARTToPngFile - haveMiRCARTToPngFile = True -except ImportError: - haveMiRCARTToPngFile = False - -try: - import base64, json, requests, urllib.request - haveUrllib = True -except ImportError: - haveUrllib = False - -class MiRCARTCanvasStore(): +class MiRCARTCanvasImportStore(): """XXX""" inFile = inSize = outMap = None parentCanvas = None @@ -62,25 +42,6 @@ class MiRCARTCanvasStore(): PS_COLOUR_DIGIT0 = 2 PS_COLOUR_DIGIT1 = 3 - # {{{ _exportFileToImgur(self, apiKey, imgName, imgTitle, pathName): upload single PNG file to Imgur - def _exportFileToImgur(self, apiKey, imgName, imgTitle, pathName): - requestImageData = open(pathName, "rb").read() - requestData = { \ - "image": base64.b64encode(requestImageData), \ - "key": apiKey, \ - "name": imgName, \ - "title": imgTitle, \ - "type": "base64"} - requestHeaders = {"Authorization": "Client-ID " + apiKey} - responseHttp = requests.post( \ - "https://api.imgur.com/3/upload.json", \ - data=requestData, headers=requestHeaders) - responseDict = json.loads(responseHttp.text) - if responseHttp.status_code == 200: - return [200, responseDict.get("data").get("link")] - else: - return [responseHttp.status_code, ""] - # }}} # {{{ _flipCellStateBit(self, cellState, bit): XXX def _flipCellStateBit(self, cellState, bit): if cellState & bit: @@ -101,65 +62,6 @@ class MiRCARTCanvasStore(): return (15, 1) # }}} - # {{{ exportBitmapToPngFile(self, canvasBitmap, outPathName, outType): XXX - def exportBitmapToPngFile(self, canvasBitmap, outPathName, outType): - return canvasBitmap.ConvertToImage().SaveFile(outPathName, outType) - # }}} - # {{{ exportBitmapToImgur(self, apiKey, canvasBitmap, imgName, imgTitle, imgType): XXX - def exportBitmapToImgur(self, apiKey, canvasBitmap, imgName, imgTitle, imgType): - tmpPathName = tempfile.mkstemp() - os.close(tmpPathName[0]) - canvasBitmap.ConvertToImage().SaveFile(tmpPathName[1], imgType) - imgurResult = self._exportFileToImgur(apiKey, imgName, imgTitle, tmpPathName[1]) - os.remove(tmpPathName[1]) - return imgurResult - # }}} - # {{{ exportPastebin(self, apiDevKey, canvasMap, canvasSize, pasteName="", pastePrivate=0): XXX - def exportPastebin(self, apiDevKey, canvasMap, canvasSize, pasteName="", pastePrivate=0): - if haveUrllib: - outFile = io.StringIO() - self.exportTextFile(canvasMap, canvasSize, outFile) - requestData = { \ - "api_dev_key": apiDevKey, \ - "api_option": "paste", \ - "api_paste_code": outFile.getvalue().encode(), \ - "api_paste_name": pasteName, \ - "api_paste_private": pastePrivate} - responseHttp = requests.post("https://pastebin.com/api/api_post.php", \ - data=requestData) - if responseHttp.status_code == 200: - if responseHttp.text.startswith("http"): - return (True, responseHttp.text) - else: - return (False, responseHttp.text) - else: - return (False, str(responseHttp.status_code)) - else: - return (False, "missing requests and/or urllib3 module(s)") - # }}} - # {{{ exportPngFile(self, canvasMap, outPathName): XXX - def exportPngFile(self, canvasMap, outPathName): - if haveMiRCARTToPngFile: - MiRCARTToPngFile(canvasMap).export(outPathName) - return True - else: - return False - # }}} - # {{{ exportTextFile(self, canvasMap, canvasSize, outFile): XXX - def exportTextFile(self, canvasMap, canvasSize, outFile): - for canvasRow in range(canvasSize[1]): - canvasLastColours = [] - for canvasCol in range(canvasSize[0]): - canvasColColours = canvasMap[canvasRow][canvasCol][0] - canvasColText = canvasMap[canvasRow][canvasCol][2] - if canvasColColours != canvasLastColours: - canvasLastColours = canvasColColours - outFile.write("\x03" + \ - str(canvasColColours[0]) + \ - "," + str(canvasColColours[1])) - outFile.write(canvasColText) - outFile.write("\n") - # }}} # {{{ importIntoPanel(self): XXX def importIntoPanel(self): self.parentCanvas.onStoreUpdate(self.inSize, self.outMap) diff --git a/MiRCARTFrame.py b/MiRCARTFrame.py index 68d1403..261cca5 100644 --- a/MiRCARTFrame.py +++ b/MiRCARTFrame.py @@ -189,7 +189,7 @@ class MiRCARTFrame(MiRCARTGeneralFrame): else: outPathName = dialog.GetPath() self.SetCursor(wx.Cursor(wx.CURSOR_WAIT)) - self.panelCanvas.canvasStore.exportBitmapToPngFile( \ + self.panelCanvas.canvasExportStore.exportBitmapToPngFile( \ self.panelCanvas.canvasBackend.canvasBitmap, outPathName, \ wx.BITMAP_TYPE_PNG) self.SetCursor(wx.Cursor(wx.NullCursor)) @@ -198,8 +198,8 @@ class MiRCARTFrame(MiRCARTGeneralFrame): # {{{ canvasExportImgur(self): XXX def canvasExportImgur(self): self.SetCursor(wx.Cursor(wx.CURSOR_WAIT)) - imgurResult = self.panelCanvas.canvasStore.exportBitmapToImgur( \ - "c9a6efb3d7932fd", self.panelCanvas.canvasBackend.canvasBitmap, \ + imgurResult = self.panelCanvas.canvasExportStore.exportBitmapToImgur( \ + "c9a6efb3d7932fd", self.panelCanvas.canvasBackend.canvasBitmap, \ "", "", wx.BITMAP_TYPE_PNG) self.SetCursor(wx.Cursor(wx.NullCursor)) if imgurResult[0] == 200: @@ -217,7 +217,7 @@ class MiRCARTFrame(MiRCARTGeneralFrame): def canvasExportPastebin(self): self.SetCursor(wx.Cursor(wx.CURSOR_WAIT)) pasteStatus, pasteResult = \ - self.panelCanvas.canvasStore.exportPastebin( \ + self.panelCanvas.canvasExportStore.exportPastebin( \ "", \ self.panelCanvas.canvasMap, \ self.panelCanvas.canvasSize) @@ -246,7 +246,7 @@ class MiRCARTFrame(MiRCARTGeneralFrame): self.SetCursor(wx.Cursor(wx.CURSOR_WAIT)) if newCanvasSize == None: newCanvasSize = (100, 30) - self.panelCanvas.canvasStore.importNew(newCanvasSize) + self.panelCanvas.canvasImportStore.importNew(newCanvasSize) self.canvasPathName = None self.SetCursor(wx.Cursor(wx.NullCursor)) self._updateStatusBar(); self.onCanvasUpdate(); @@ -268,8 +268,8 @@ class MiRCARTFrame(MiRCARTGeneralFrame): else: self.canvasPathName = dialog.GetPath() self.SetCursor(wx.Cursor(wx.CURSOR_WAIT)) - self.panelCanvas.canvasStore.importTextFile(self.canvasPathName) - self.panelCanvas.canvasStore.importIntoPanel() + self.panelCanvas.canvasImportStore.importTextFile(self.canvasPathName) + self.panelCanvas.canvasImportStore.importIntoPanel() self.SetCursor(wx.Cursor(wx.NullCursor)) self._updateStatusBar(); self.onCanvasUpdate(); return True @@ -282,7 +282,7 @@ class MiRCARTFrame(MiRCARTGeneralFrame): try: with open(self.canvasPathName, "w") as outFile: self.SetCursor(wx.Cursor(wx.CURSOR_WAIT)) - self.panelCanvas.canvasStore.exportTextFile( \ + self.panelCanvas.canvasExportStore.exportTextFile( \ self.panelCanvas.canvasMap, \ self.panelCanvas.canvasSize, outFile) self.SetCursor(wx.Cursor(wx.NullCursor)) diff --git a/MiRCARTToPngFile.py b/MiRCARTToPngFile.py index 78ef788..ed76ccc 100755 --- a/MiRCARTToPngFile.py +++ b/MiRCARTToPngFile.py @@ -22,7 +22,7 @@ # SOFTWARE. # -import MiRCARTCanvasStore +import MiRCARTCanvasImportStore from PIL import Image, ImageDraw, ImageFont import sys @@ -128,7 +128,7 @@ class MiRCARTToPngFile: # # Entry point def main(*argv): - canvasStore = MiRCARTCanvasStore.MiRCARTCanvasStore(inFile=argv[1]) + canvasStore = MiRCARTCanvasImportStore.MiRCARTCanvasImportStore(inFile=argv[1]) MiRCARTToPngFile(canvasStore.outMap, *argv[3:]).export(argv[2]) if __name__ == "__main__": if ((len(sys.argv) - 1) < 2)\ From 3e031260066233a56c72a1b95ee24567f1b0f0fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Wed, 10 Jan 2018 02:30:32 +0100 Subject: [PATCH 096/148] MiRCARTCanvasInterface.py: split from MiRCARTCanvas.py. MiRCART{Canvas,{,General}Frame}.py: updated. --- MiRCARTCanvas.py | 32 ++-- MiRCARTCanvasBackend.py | 4 +- MiRCARTCanvasInterface.py | 272 +++++++++++++++++++++++++++++ MiRCARTFrame.py | 349 +++++++------------------------------- MiRCARTGeneralFrame.py | 69 ++++---- 5 files changed, 385 insertions(+), 341 deletions(-) create mode 100644 MiRCARTCanvasInterface.py diff --git a/MiRCARTCanvas.py b/MiRCARTCanvas.py index 0ad4ff2..5de4b44 100644 --- a/MiRCARTCanvas.py +++ b/MiRCARTCanvas.py @@ -26,7 +26,7 @@ from MiRCARTCanvasBackend import MiRCARTCanvasBackend from MiRCARTCanvasJournal import MiRCARTCanvasJournal from MiRCARTCanvasExportStore import MiRCARTCanvasExportStore, haveMiRCARTToPngFile, haveUrllib from MiRCARTCanvasImportStore import MiRCARTCanvasImportStore -from MiRCARTColours import MiRCARTColours +from MiRCARTCanvasInterface import MiRCARTCanvasInterface import wx class MiRCARTCanvas(wx.Panel): @@ -34,8 +34,9 @@ class MiRCARTCanvas(wx.Panel): parentFrame = None canvasMap = canvasPos = canvasSize = None brushColours = brushPos = brushSize = None - canvasBackend = canvasCurTool = canvasJournal = None + canvasBackend = canvasJournal = None canvasExportStore = canvasImportStore = None + canvasInterface = None # {{{ _commitPatch(self, patch): XXX def _commitPatch(self, patch): @@ -47,7 +48,7 @@ class MiRCARTCanvas(wx.Panel): for patch in deltaPatches: if self.canvasBackend.drawPatch(eventDc, patch): self._commitPatch(patch) - self.parentFrame.onCanvasUpdate() + self.parentFrame.onUndoUpdate() # }}} # {{{ _dispatchPatch(self, eventDc, isCursor, patch): XXX def _dispatchPatch(self, eventDc, isCursor, patch): @@ -72,12 +73,16 @@ class MiRCARTCanvas(wx.Panel): def onPanelClose(self, event): self.Destroy() # }}} + # {{{ onPanelEnterWindow(self, event): XXX + def onPanelEnterWindow(self, event): + self.parentFrame.SetFocus() + # }}} # {{{ onPanelInput(self, event): XXX def onPanelInput(self, event): eventDc = self.canvasBackend.getDeviceContext(self) eventType = event.GetEventType() self._canvasDirty = self._canvasDirtyCursor = False - tool = self.canvasCurTool + tool = self.canvasInterface.canvasTool if eventType == wx.wxEVT_CHAR: mapPoint = self.brushPos doSkip = tool.onKeyboardEvent( \ @@ -93,15 +98,15 @@ class MiRCARTCanvas(wx.Panel): event.Dragging(), event.LeftIsDown(), event.RightIsDown(), \ self._dispatchPatch, eventDc) if self._canvasDirty: - self.parentFrame.onCanvasUpdate() - self.parentFrame.onCanvasMotion(event, mapPoint) + self.parentFrame.onUndoUpdate() + if eventType == wx.wxEVT_MOTION: + self.parentFrame.onStatusBarUpdate(showPos=mapPoint) # }}} # {{{ onPanelLeaveWindow(self, event): XXX def onPanelLeaveWindow(self, event): eventDc = self.canvasBackend.getDeviceContext(self) self.canvasBackend.drawCursorMaskWithJournal( \ self.canvasJournal, eventDc) - self.parentFrame.onCanvasMotion(event) # }}} # {{{ onPanelPaint(self, event): XXX def onPanelPaint(self, event): @@ -123,14 +128,6 @@ class MiRCARTCanvas(wx.Panel): *self.canvasMap[numRow][numCol])) wx.SafeYield() # }}} - # {{{ popRedo(self): XXX - def popRedo(self): - self._dispatchDeltaPatches(self.canvasJournal.popRedo()) - # }}} - # {{{ popUndo(self): XXX - def popUndo(self): - self._dispatchDeltaPatches(self.canvasJournal.popUndo()) - # }}} # {{{ resize(self, newCanvasSize): XXX def resize(self, newCanvasSize): if newCanvasSize != self.canvasSize: @@ -152,7 +149,7 @@ class MiRCARTCanvas(wx.Panel): self.canvasBackend.cellSize)]) self.canvasBackend.reset(self.canvasSize, self.canvasBackend.cellSize) self.canvasJournal.resetCursor(); self.canvasJournal.resetUndo(); - self.parentFrame.onCanvasUpdate() + self.parentFrame.onUndoUpdate() # }}} # @@ -165,13 +162,14 @@ class MiRCARTCanvas(wx.Panel): self.canvasMap = None; self.canvasPos = canvasPos; self.canvasSize = canvasSize; self.brushColours = [4, 1]; self.brushPos = [0, 0]; self.brushSize = [1, 1]; self.canvasBackend = MiRCARTCanvasBackend(canvasSize, cellSize) - self.canvasCurTool = None self.canvasJournal = MiRCARTCanvasJournal() self.canvasExportStore = MiRCARTCanvasExportStore(parentCanvas=self) self.canvasImportStore = MiRCARTCanvasImportStore(parentCanvas=self) + self.canvasInterface = MiRCARTCanvasInterface(self, parentFrame) # Bind event handlers self.Bind(wx.EVT_CLOSE, self.onPanelClose) + self.Bind(wx.EVT_ENTER_WINDOW, self.onPanelEnterWindow) self.Bind(wx.EVT_LEAVE_WINDOW, self.onPanelLeaveWindow) self.parentFrame.Bind(wx.EVT_CHAR, self.onPanelInput) for eventType in( \ diff --git a/MiRCARTCanvasBackend.py b/MiRCARTCanvasBackend.py index 192cc13..154b0ca 100644 --- a/MiRCARTCanvasBackend.py +++ b/MiRCARTCanvasBackend.py @@ -97,7 +97,9 @@ class MiRCARTCanvasBackend(): # {{{ drawPatch(self, eventDc, patch): XXX def drawPatch(self, eventDc, patch): if patch[0][0] < self.canvasSize[0] \ - and patch[0][1] < self.canvasSize[1]: + and patch[0][0] >= 0 \ + and patch[0][1] < self.canvasSize[1] \ + and patch[0][1] >= 0: if patch[3] == " ": self._drawBrushPatch(eventDc, patch) else: diff --git a/MiRCARTCanvasInterface.py b/MiRCARTCanvasInterface.py new file mode 100644 index 0000000..fc30fec --- /dev/null +++ b/MiRCARTCanvasInterface.py @@ -0,0 +1,272 @@ +#!/usr/bin/env python3 +# +# MiRCARTCanvasInterface.py -- XXX +# Copyright (c) 2018 Lucio Andrés Illanes Albornoz +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +from MiRCARTToolCircle import MiRCARTToolCircle +from MiRCARTToolLine import MiRCARTToolLine +from MiRCARTToolRect import MiRCARTToolRect +from MiRCARTToolText import MiRCARTToolText + +import os, wx + +class MiRCARTCanvasInterface(): + """XXX""" + parentCanvas = parentFrame = canvasPathName = canvasTool = None + + # {{{ _dialogSaveChanges(self) + def _dialogSaveChanges(self): + with wx.MessageDialog(self.parentCanvas, \ + "Do you want to save changes to {}?".format( \ + self.canvasPathName), "MiRCART", \ + wx.CANCEL|wx.CANCEL_DEFAULT|wx.ICON_QUESTION|wx.YES_NO) as dialog: + dialogChoice = dialog.ShowModal() + return dialogChoice + # }}} + # {{{ _updateCanvasSize(self, newCanvasSize): XXX + def _updateCanvasSize(self, newCanvasSize): + eventDc = self.parentCanvas.canvasBackend.getDeviceContext(self.parentCanvas) + self.parentCanvas.resize(newCanvasSize) + self.parentCanvas.canvasBackend.resize( \ + newCanvasSize, \ + self.parentCanvas.canvasBackend.cellSize) + for numRow in range(self.parentCanvas.canvasSize[1] - 1): + self.parentCanvas.canvasMap.append([[1, 1], 0, " "]) + self.parentCanvas.canvasMap.append([]) + for numCol in range(self.parentCanvas.canvasSize[0]): + self.parentCanvas.canvasMap[-1].append([[1, 1], 0, " "]) + self.parentCanvas.canvasBackend.drawPatch(eventDc, \ + ([numCol, self.parentCanvas.canvasSize[1] - 1], *[[1, 1], 0, " "])) + wx.SafeYield() + # }}} + + # {{{ canvasBrushSolid(self, event): XXX + def canvasBrushSolid(self, event): + pass + # }}} + # {{{ canvasColour(self, event, numColour): XXX + def canvasColour(self, event, numColour): + if event.GetEventType() == wx.wxEVT_TOOL: + self.parentCanvas.brushColours[0] = numColour + elif event.GetEventType() == wx.wxEVT_TOOL_RCLICKED: + self.parentCanvas.brushColours[1] = numColour + self.parentFrame.onStatusBarUpdate() + # }}} + # {{{ canvasCopy(self, event): XXX + def canvasCopy(self, event): + pass + # }}} + # {{{ canvasCut(self, event): XXX + def canvasCut(self, event): + pass + # }}} + # {{{ canvasDecrBrush(self, event): XXX + def canvasDecrBrush(self, event): + if self.parentCanvas.brushSize[0] > 1 \ + and self.parentCanvas.brushSize[1] > 1: + self.parentCanvas.brushSize = \ + [a-1 for a in self.parentCanvas.brushSize] + # }}} + # {{{ canvasDecrCanvas(self, event): XXX + def canvasDecrCanvas(self, event): + newCanvasSize = [a-1 if a > 1 else a for a in self.parentCanvas.canvasSize] + self._updateCanvasSize(newCanvasSize) + # }}} + # {{{ canvasDelete(self, event): XXX + def canvasDelete(self, event): + pass + # }}} + # {{{ canvasExit(self, event): XXX + def canvasExit(self, event): + self.parentFrame.Close(True) + # }}} + # {{{ canvasExportAsPng(self, event): XXX + def canvasExportAsPng(self, event): + with wx.FileDialog(self, "Save As...", os.getcwd(), "", \ + "*.png", wx.FD_SAVE|wx.FD_OVERWRITE_PROMPT) as dialog: + if dialog.ShowModal() == wx.ID_CANCEL: + return False + else: + outPathName = dialog.GetPath() + self.SetCursor(wx.Cursor(wx.CURSOR_WAIT)) + self.parentCanvas.canvasExportStore.exportBitmapToPngFile( \ + self.parentCanvas.canvasBackend.canvasBitmap, outPathName, \ + wx.BITMAP_TYPE_PNG) + self.SetCursor(wx.Cursor(wx.NullCursor)) + return True + # }}} + # {{{ canvasExportImgur(self, event): XXX + def canvasExportImgur(self, event): + self.parentCanvas.SetCursor(wx.Cursor(wx.CURSOR_WAIT)) + imgurResult = self.parentCanvas.canvasExportStore.exportBitmapToImgur( \ + "c9a6efb3d7932fd", self.parentCanvas.canvasBackend.canvasBitmap, \ + "", "", wx.BITMAP_TYPE_PNG) + self.parentCanvas.SetCursor(wx.Cursor(wx.NullCursor)) + if imgurResult[0] == 200: + if not wx.TheClipboard.IsOpened(): + wx.TheClipboard.Open() + wx.TheClipboard.SetData(wx.TextDataObject(imgurResult[1])) + wx.TheClipboard.Close() + wx.MessageBox("Exported to Imgur: " + imgurResult[1], \ + "Export to Imgur", wx.OK|wx.ICON_INFORMATION) + else: + wx.MessageBox("Failed to export to Imgur: " + imgurResult[1], \ + "Export to Imgur", wx.OK|wx.ICON_EXCLAMATION) + # }}} + # {{{ canvasExportPastebin(self, event): XXX + def canvasExportPastebin(self, event): + self.SetCursor(wx.Cursor(wx.CURSOR_WAIT)) + pasteStatus, pasteResult = \ + self.parentCanvas.canvasExportStore.exportPastebin( \ + "", \ + self.parentCanvas.canvasMap, \ + self.parentCanvas.canvasSize) + self.SetCursor(wx.Cursor(wx.NullCursor)) + if pasteStatus: + if not wx.TheClipboard.IsOpened(): + wx.TheClipboard.Open() + wx.TheClipboard.SetData(wx.TextDataObject(pasteResult)) + wx.TheClipboard.Close() + wx.MessageBox("Exported to Pastebin: " + pasteResult, \ + "Export to Pastebin", wx.OK|wx.ICON_INFORMATION) + else: + wx.MessageBox("Failed to export to Pastebin: " + pasteResult, \ + "Export to Pastebin", wx.OK|wx.ICON_EXCLAMATION) + # }}} + # {{{ canvasIncrBrush(self, event): XXX + def canvasIncrBrush(self, event): + self.parentCanvas.brushSize = \ + [a+1 for a in self.parentCanvas.brushSize] + # }}} + # {{{ canvasIncrCanvas(self, event): XXX + def canvasIncrCanvas(self, event): + newCanvasSize = [a+1 for a in self.parentCanvas.canvasSize] + self._updateCanvasSize(newCanvasSize) + # }}} + # {{{ canvasNew(self, event, newCanvasSize=None): XXX + def canvasNew(self, event, newCanvasSize=None): + if self.canvasPathName != None: + saveChanges = self._dialogSaveChanges() + if saveChanges == wx.ID_CANCEL: + return + elif saveChanges == wx.ID_NO: + pass + elif saveChanges == wx.ID_YES: + self.canvasSave() + self.parentCanvas.SetCursor(wx.Cursor(wx.CURSOR_WAIT)) + if newCanvasSize == None: + newCanvasSize = (100, 30) + self.parentCanvas.canvasImportStore.importNew(newCanvasSize) + self.canvasPathName = None + self.parentCanvas.SetCursor(wx.Cursor(wx.NullCursor)) + self.parentFrame.onStatusBarUpdate() + self.parentFrame.onUndoUpdate() + # }}} + # {{{ canvasOpen(self, event): XXX + def canvasOpen(self, event): + if self.canvasPathName != None: + saveChanges = self._dialogSaveChanges() + if saveChanges == wx.ID_CANCEL: + return + elif saveChanges == wx.ID_NO: + pass + elif saveChanges == wx.ID_YES: + self.canvasSave() + with wx.FileDialog(self.parentCanvas, "Open", os.getcwd(), "", \ + "*.txt", wx.FD_OPEN) as dialog: + if dialog.ShowModal() == wx.ID_CANCEL: + return False + else: + self.canvasPathName = dialog.GetPath() + self.parentCanvas.SetCursor(wx.Cursor(wx.CURSOR_WAIT)) + self.parentCanvas.canvasImportStore.importTextFile(self.canvasPathName) + self.parentCanvas.canvasImportStore.importIntoPanel() + self.parentCanvas.SetCursor(wx.Cursor(wx.NullCursor)) + self.parentFrame.onStatusBarUpdate(showFileName=self.canvasPathName) + self.parentFrame.onUndoUpdate() + return True + # }}} + # {{{ canvasPaste(self, event): XXX + def canvasPaste(self, event): + pass + # }}} + # {{{ canvasRedo(self, event): XXX + def canvasRedo(self, event): + self.parentCanvas._dispatchDeltaPatches( \ + self.parentCanvas.canvasJournal.popRedo()) + # }}} + # {{{ canvasSave(self, event): XXX + def canvasSave(self, event): + if self.canvasPathName == None: + if self.canvasSaveAs(event) == False: + return + try: + with open(self.canvasPathName, "w") as outFile: + self.SetCursor(wx.Cursor(wx.CURSOR_WAIT)) + self.parentCanvas.canvasExportStore.exportTextFile( \ + self.parentCanvas.canvasMap, \ + self.parentCanvas.canvasSize, outFile) + self.SetCursor(wx.Cursor(wx.NullCursor)) + return True + except IOError as error: + return False + # }}} + # {{{ canvasSaveAs(self, event): XXX + def canvasSaveAs(self, event): + with wx.FileDialog(self.parentCanvas, "Save As", os.getcwd(), "", \ + "*.txt", wx.FD_SAVE|wx.FD_OVERWRITE_PROMPT) as dialog: + if dialog.ShowModal() == wx.ID_CANCEL: + return False + else: + self.canvasPathName = dialog.GetPath() + return self.canvasSave(event) + # }}} + # {{{ canvasToolCircle(self, event): XXX + def canvasToolCircle(self, event): + self.canvasTool = MiRCARTToolCircle(self.parentCanvas) + # }}} + # {{{ canvasToolLine(self, event): XXX + def canvasToolLine(self, event): + self.canvasTool = MiRCARTToolLine(self.parentCanvas) + # }}} + # {{{ canvasToolRect(self, event): XXX + def canvasToolRect(self, event): + self.canvasTool = MiRCARTToolRect(self.parentCanvas) + # }}} + # {{{ canvasToolText(self, event): XXX + def canvasToolText(self, event): + self.canvasTool = MiRCARTToolText(self.parentCanvas) + # }}} + # {{{ canvasUndo(self, event): XXX + def canvasUndo(self, event): + self.parentCanvas._dispatchDeltaPatches( \ + self.parentCanvas.canvasJournal.popUndo()) + # }}} + + # + # __init__(self, parentCanvas, parentFrame): + def __init__(self, parentCanvas, parentFrame): + self.parentCanvas = parentCanvas; self.parentFrame = parentFrame; + self.canvasPathName = None + self.canvasToolRect(None) + +# vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/MiRCARTFrame.py b/MiRCARTFrame.py index 261cca5..1d7b3be 100644 --- a/MiRCARTFrame.py +++ b/MiRCARTFrame.py @@ -23,74 +23,68 @@ # from MiRCARTCanvas import MiRCARTCanvas, haveUrllib +from MiRCARTCanvasInterface import MiRCARTCanvasInterface from MiRCARTColours import MiRCARTColours -from MiRCARTGeneralFrame import MiRCARTGeneralFrame, \ - TID_ACCELS, TID_COMMAND, TID_LIST, TID_MENU, TID_NOTHING, TID_SELECT, TID_TOOLBAR -from MiRCARTToolCircle import MiRCARTToolCircle -from MiRCARTToolLine import MiRCARTToolLine -from MiRCARTToolRect import MiRCARTToolRect -from MiRCARTToolText import MiRCARTToolText - +from MiRCARTGeneralFrame import MiRCARTGeneralFrame, \ + TID_ACCELS, TID_COMMAND, TID_LIST, TID_MENU, TID_NOTHING, TID_SELECT, TID_TOOLBAR, \ + NID_MENU_SEP, NID_TOOLBAR_SEP + import os, wx class MiRCARTFrame(MiRCARTGeneralFrame): """XXX""" - panelCanvas = canvasPathName = None + panelCanvas = None # {{{ Commands # Id Type Id Labels Icon bitmap Accelerator [Initial state] - CID_NEW = [0x100, TID_COMMAND, "New", "&New", ["", wx.ART_NEW], [wx.ACCEL_CTRL, ord("N")]] - CID_OPEN = [0x101, TID_COMMAND, "Open", "&Open", ["", wx.ART_FILE_OPEN], [wx.ACCEL_CTRL, ord("O")]] - CID_SAVE = [0x102, TID_COMMAND, "Save", "&Save", ["", wx.ART_FILE_SAVE], [wx.ACCEL_CTRL, ord("S")]] - CID_SAVEAS = [0x103, TID_COMMAND, "Save As...", "Save &As...", ["", wx.ART_FILE_SAVE_AS], None] + CID_NEW = [0x100, TID_COMMAND, "New", "&New", ["", wx.ART_NEW], [wx.ACCEL_CTRL, ord("N")], None, MiRCARTCanvasInterface.canvasNew] + CID_OPEN = [0x101, TID_COMMAND, "Open", "&Open", ["", wx.ART_FILE_OPEN], [wx.ACCEL_CTRL, ord("O")], None, MiRCARTCanvasInterface.canvasOpen] + CID_SAVE = [0x102, TID_COMMAND, "Save", "&Save", ["", wx.ART_FILE_SAVE], [wx.ACCEL_CTRL, ord("S")], None, MiRCARTCanvasInterface.canvasSave] + CID_SAVEAS = [0x103, TID_COMMAND, "Save As...", "Save &As...", ["", wx.ART_FILE_SAVE_AS], None, None, MiRCARTCanvasInterface.canvasSaveAs] CID_EXPORT_AS_PNG = [0x104, TID_COMMAND, "Export as PNG...", \ - "Export as PN&G...", None, None] + "Export as PN&G...", None, None, None, MiRCARTCanvasInterface.canvasExportAsPng] CID_EXPORT_IMGUR = [0x105, TID_COMMAND, "Export to Imgur...", \ - "Export to I&mgur...", None, None, haveUrllib] + "Export to I&mgur...", None, None, haveUrllib, MiRCARTCanvasInterface.canvasExportImgur] CID_EXPORT_PASTEBIN = [0x106, TID_COMMAND, "Export to Pastebin...", \ - "Export to Pasteb&in...", None, None, haveUrllib] - CID_EXIT = [0x107, TID_COMMAND, "Exit", "E&xit", None, None] - CID_UNDO = [0x108, TID_COMMAND, "Undo", "&Undo", ["", wx.ART_UNDO], [wx.ACCEL_CTRL, ord("Z")], False] - CID_REDO = [0x109, TID_COMMAND, "Redo", "&Redo", ["", wx.ART_REDO], [wx.ACCEL_CTRL, ord("Y")], False] - CID_CUT = [0x10a, TID_COMMAND, "Cut", "Cu&t", ["", wx.ART_CUT], None, False] - CID_COPY = [0x10b, TID_COMMAND, "Copy", "&Copy", ["", wx.ART_COPY], None, False] - CID_PASTE = [0x10c, TID_COMMAND, "Paste", "&Paste", ["", wx.ART_PASTE], None, False] - CID_DELETE = [0x10d, TID_COMMAND, "Delete", "De&lete", ["", wx.ART_DELETE], None, False] + "Export to Pasteb&in...", None, None, haveUrllib, MiRCARTCanvasInterface.canvasExportPastebin] + CID_EXIT = [0x107, TID_COMMAND, "Exit", "E&xit", None, None, None, MiRCARTCanvasInterface.canvasExit] + CID_UNDO = [0x108, TID_COMMAND, "Undo", "&Undo", ["", wx.ART_UNDO], [wx.ACCEL_CTRL, ord("Z")], False, MiRCARTCanvasInterface.canvasUndo] + CID_REDO = [0x109, TID_COMMAND, "Redo", "&Redo", ["", wx.ART_REDO], [wx.ACCEL_CTRL, ord("Y")], False, MiRCARTCanvasInterface.canvasRedo] + CID_CUT = [0x10a, TID_COMMAND, "Cut", "Cu&t", ["", wx.ART_CUT], None, False, MiRCARTCanvasInterface.canvasCut] + CID_COPY = [0x10b, TID_COMMAND, "Copy", "&Copy", ["", wx.ART_COPY], None, False, MiRCARTCanvasInterface.canvasCopy] + CID_PASTE = [0x10c, TID_COMMAND, "Paste", "&Paste", ["", wx.ART_PASTE], None, False, MiRCARTCanvasInterface.canvasPaste] + CID_DELETE = [0x10d, TID_COMMAND, "Delete", "De&lete", ["", wx.ART_DELETE], None, False, MiRCARTCanvasInterface.canvasDelete] CID_INCR_CANVAS = [0x10e, TID_COMMAND, "Increase canvas size", \ - "I&ncrease canvas size", ["", wx.ART_PLUS], [wx.ACCEL_ALT, ord("+")]] + "I&ncrease canvas size", ["", wx.ART_PLUS], [wx.ACCEL_ALT, ord("+")], None, MiRCARTCanvasInterface.canvasIncrCanvas] CID_DECR_CANVAS = [0x10f, TID_COMMAND, "Decrease canvas size", \ - "D&ecrease canvas size", ["", wx.ART_MINUS], [wx.ACCEL_ALT, ord("-")]] + "D&ecrease canvas size", ["", wx.ART_MINUS], [wx.ACCEL_ALT, ord("-")], None, MiRCARTCanvasInterface.canvasDecrCanvas] CID_INCR_BRUSH = [0x110, TID_COMMAND, "Increase brush size", \ - "&Increase brush size", ["", wx.ART_PLUS], [wx.ACCEL_CTRL, ord("+")]] + "&Increase brush size", ["", wx.ART_PLUS], [wx.ACCEL_CTRL, ord("+")], None, MiRCARTCanvasInterface.canvasIncrBrush] CID_DECR_BRUSH = [0x111, TID_COMMAND, "Decrease brush size", \ - "&Decrease brush size", ["", wx.ART_MINUS], [wx.ACCEL_CTRL, ord("-")]] - CID_SOLID_BRUSH = [0x112, TID_SELECT, "Solid brush", "&Solid brush", None, None, True] + "&Decrease brush size", ["", wx.ART_MINUS], [wx.ACCEL_CTRL, ord("-")], None, MiRCARTCanvasInterface.canvasDecrBrush] + CID_SOLID_BRUSH = [0x112, TID_SELECT, "Solid brush", "&Solid brush", None, None, True, MiRCARTCanvasInterface.canvasBrushSolid] - CID_RECT = [0x150, TID_SELECT, "Rectangle", "&Rectangle", ["toolRect.png"], [wx.ACCEL_CTRL, ord("R")], True] - CID_CIRCLE = [0x151, TID_SELECT, "Circle", "&Circle", ["toolCircle.png"], [wx.ACCEL_CTRL, ord("C")], False] - CID_LINE = [0x152, TID_SELECT, "Line", "&Line", ["toolLine.png"], [wx.ACCEL_CTRL, ord("L")], False] - CID_TEXT = [0x153, TID_SELECT, "Text", "&Text", ["toolText.png"], [wx.ACCEL_CTRL, ord("T")], False] + CID_RECT = [0x150, TID_SELECT, "Rectangle", "&Rectangle", ["toolRect.png"], [wx.ACCEL_CTRL, ord("R")], True, MiRCARTCanvasInterface.canvasToolRect] + CID_CIRCLE = [0x151, TID_SELECT, "Circle", "&Circle", ["toolCircle.png"], [wx.ACCEL_CTRL, ord("C")], False, MiRCARTCanvasInterface.canvasToolCircle] + CID_LINE = [0x152, TID_SELECT, "Line", "&Line", ["toolLine.png"], [wx.ACCEL_CTRL, ord("L")], False, MiRCARTCanvasInterface.canvasToolLine] + CID_TEXT = [0x153, TID_SELECT, "Text", "&Text", ["toolText.png"], [wx.ACCEL_CTRL, ord("T")], False, MiRCARTCanvasInterface.canvasToolText] - CID_COLOUR00 = [0x1a0, TID_COMMAND, "Colour #00", "Colour #00", None, None] - CID_COLOUR01 = [0x1a1, TID_COMMAND, "Colour #01", "Colour #01", None, None] - CID_COLOUR02 = [0x1a2, TID_COMMAND, "Colour #02", "Colour #02", None, None] - CID_COLOUR03 = [0x1a3, TID_COMMAND, "Colour #03", "Colour #03", None, None] - CID_COLOUR04 = [0x1a4, TID_COMMAND, "Colour #04", "Colour #04", None, None] - CID_COLOUR05 = [0x1a5, TID_COMMAND, "Colour #05", "Colour #05", None, None] - CID_COLOUR06 = [0x1a6, TID_COMMAND, "Colour #06", "Colour #06", None, None] - CID_COLOUR07 = [0x1a7, TID_COMMAND, "Colour #07", "Colour #07", None, None] - CID_COLOUR08 = [0x1a8, TID_COMMAND, "Colour #08", "Colour #08", None, None] - CID_COLOUR09 = [0x1a9, TID_COMMAND, "Colour #09", "Colour #09", None, None] - CID_COLOUR10 = [0x1aa, TID_COMMAND, "Colour #10", "Colour #10", None, None] - CID_COLOUR11 = [0x1ab, TID_COMMAND, "Colour #11", "Colour #11", None, None] - CID_COLOUR12 = [0x1ac, TID_COMMAND, "Colour #12", "Colour #12", None, None] - CID_COLOUR13 = [0x1ad, TID_COMMAND, "Colour #13", "Colour #13", None, None] - CID_COLOUR14 = [0x1ae, TID_COMMAND, "Colour #14", "Colour #14", None, None] - CID_COLOUR15 = [0x1af, TID_COMMAND, "Colour #15", "Colour #15", None, None] - # }}} - # {{{ Non-items - NID_MENU_SEP = (0x200, TID_NOTHING) - NID_TOOLBAR_SEP = (0x201, TID_NOTHING) + CID_COLOUR00 = [0x1a0, TID_COMMAND, "Colour #00", "Colour #00", None, None, None, MiRCARTCanvasInterface.canvasColour] + CID_COLOUR01 = [0x1a1, TID_COMMAND, "Colour #01", "Colour #01", None, None, None, MiRCARTCanvasInterface.canvasColour] + CID_COLOUR02 = [0x1a2, TID_COMMAND, "Colour #02", "Colour #02", None, None, None, MiRCARTCanvasInterface.canvasColour] + CID_COLOUR03 = [0x1a3, TID_COMMAND, "Colour #03", "Colour #03", None, None, None, MiRCARTCanvasInterface.canvasColour] + CID_COLOUR04 = [0x1a4, TID_COMMAND, "Colour #04", "Colour #04", None, None, None, MiRCARTCanvasInterface.canvasColour] + CID_COLOUR05 = [0x1a5, TID_COMMAND, "Colour #05", "Colour #05", None, None, None, MiRCARTCanvasInterface.canvasColour] + CID_COLOUR06 = [0x1a6, TID_COMMAND, "Colour #06", "Colour #06", None, None, None, MiRCARTCanvasInterface.canvasColour] + CID_COLOUR07 = [0x1a7, TID_COMMAND, "Colour #07", "Colour #07", None, None, None, MiRCARTCanvasInterface.canvasColour] + CID_COLOUR08 = [0x1a8, TID_COMMAND, "Colour #08", "Colour #08", None, None, None, MiRCARTCanvasInterface.canvasColour] + CID_COLOUR09 = [0x1a9, TID_COMMAND, "Colour #09", "Colour #09", None, None, None, MiRCARTCanvasInterface.canvasColour] + CID_COLOUR10 = [0x1aa, TID_COMMAND, "Colour #10", "Colour #10", None, None, None, MiRCARTCanvasInterface.canvasColour] + CID_COLOUR11 = [0x1ab, TID_COMMAND, "Colour #11", "Colour #11", None, None, None, MiRCARTCanvasInterface.canvasColour] + CID_COLOUR12 = [0x1ac, TID_COMMAND, "Colour #12", "Colour #12", None, None, None, MiRCARTCanvasInterface.canvasColour] + CID_COLOUR13 = [0x1ad, TID_COMMAND, "Colour #13", "Colour #13", None, None, None, MiRCARTCanvasInterface.canvasColour] + CID_COLOUR14 = [0x1ae, TID_COMMAND, "Colour #14", "Colour #14", None, None, None, MiRCARTCanvasInterface.canvasColour] + CID_COLOUR15 = [0x1af, TID_COMMAND, "Colour #15", "Colour #15", None, None, None, MiRCARTCanvasInterface.canvasColour] # }}} # {{{ Menus MID_FILE = (0x300, TID_MENU, "File", "&File", ( \ @@ -147,23 +141,25 @@ class MiRCARTFrame(MiRCARTGeneralFrame): toolBitmapDc.DrawRectangle(0, 0, 16, 16) paletteDescr[numColour][4] = ["", None, toolBitmap] # }}} - # {{{ _dialogSaveChanges(self) - def _dialogSaveChanges(self): - with wx.MessageDialog(self, \ - "Do you want to save changes to {}?".format( \ - self.canvasPathName), "MiRCART", \ - wx.CANCEL|wx.CANCEL_DEFAULT|wx.ICON_QUESTION|wx.YES_NO) as dialog: - dialogChoice = dialog.ShowModal() - return dialogChoice + + # {{{ onInput(self, event): XXX + def onInput(self, event): + eventId = event.GetId() + if eventId >= self.CID_COLOUR00[0] \ + and eventId <= self.CID_COLOUR15[0]: + numColour = eventId - self.CID_COLOUR00[0] + self.itemsById[eventId][7](self.panelCanvas.canvasInterface, event, numColour) + else: + self.itemsById[eventId][7](self.panelCanvas.canvasInterface, event) # }}} - # {{{ _updateStatusBar(self, showColours=None, showFileName=True, showPos=None): XXX - def _updateStatusBar(self, showColours=True, showFileName=True, showPos=True): + # {{{ onStatusBarUpdate(self, showColours=None, showFileName=True, showPos=None): XXX + def onStatusBarUpdate(self, showColours=True, showFileName=True, showPos=True): if showColours == True: showColours = self.panelCanvas.brushColours if showPos == True: showPos = self.panelCanvas.brushPos if showFileName == True: - showFileName = self.canvasPathName + showFileName = self.panelCanvas.canvasInterface.canvasPathName textItems = [] if showPos != None: textItems.append("X: {:03d} Y: {:03d}".format( \ @@ -179,139 +175,8 @@ class MiRCARTFrame(MiRCARTGeneralFrame): os.path.basename(showFileName))) self.statusBar.SetStatusText(" | ".join(textItems)) # }}} - - # {{{ canvasExportAsPng(self): XXX - def canvasExportAsPng(self): - with wx.FileDialog(self, self.CID_SAVEAS[2], os.getcwd(), "", \ - "*.png", wx.FD_SAVE|wx.FD_OVERWRITE_PROMPT) as dialog: - if dialog.ShowModal() == wx.ID_CANCEL: - return False - else: - outPathName = dialog.GetPath() - self.SetCursor(wx.Cursor(wx.CURSOR_WAIT)) - self.panelCanvas.canvasExportStore.exportBitmapToPngFile( \ - self.panelCanvas.canvasBackend.canvasBitmap, outPathName, \ - wx.BITMAP_TYPE_PNG) - self.SetCursor(wx.Cursor(wx.NullCursor)) - return True - # }}} - # {{{ canvasExportImgur(self): XXX - def canvasExportImgur(self): - self.SetCursor(wx.Cursor(wx.CURSOR_WAIT)) - imgurResult = self.panelCanvas.canvasExportStore.exportBitmapToImgur( \ - "c9a6efb3d7932fd", self.panelCanvas.canvasBackend.canvasBitmap, \ - "", "", wx.BITMAP_TYPE_PNG) - self.SetCursor(wx.Cursor(wx.NullCursor)) - if imgurResult[0] == 200: - if not wx.TheClipboard.IsOpened(): - wx.TheClipboard.Open() - wx.TheClipboard.SetData(wx.TextDataObject(imgurResult[1])) - wx.TheClipboard.Close() - wx.MessageBox("Exported to Imgur: " + imgurResult[1], \ - "Export to Imgur", wx.OK|wx.ICON_INFORMATION) - else: - wx.MessageBox("Failed to export to Imgur: " + imgurResult[1], \ - "Export to Imgur", wx.OK|wx.ICON_EXCLAMATION) - # }}} - # {{{ canvasExportPastebin(self): XXX - def canvasExportPastebin(self): - self.SetCursor(wx.Cursor(wx.CURSOR_WAIT)) - pasteStatus, pasteResult = \ - self.panelCanvas.canvasExportStore.exportPastebin( \ - "", \ - self.panelCanvas.canvasMap, \ - self.panelCanvas.canvasSize) - self.SetCursor(wx.Cursor(wx.NullCursor)) - if pasteStatus: - if not wx.TheClipboard.IsOpened(): - wx.TheClipboard.Open() - wx.TheClipboard.SetData(wx.TextDataObject(pasteResult)) - wx.TheClipboard.Close() - wx.MessageBox("Exported to Pastebin: " + pasteResult, \ - "Export to Pastebin", wx.OK|wx.ICON_INFORMATION) - else: - wx.MessageBox("Failed to export to Pastebin: " + pasteResult, \ - "Export to Pastebin", wx.OK|wx.ICON_EXCLAMATION) - # }}} - # {{{ canvasNew(self, newCanvasSize=None): XXX - def canvasNew(self, newCanvasSize=None): - if self.canvasPathName != None: - saveChanges = self._dialogSaveChanges() - if saveChanges == wx.ID_CANCEL: - return - elif saveChanges == wx.ID_NO: - pass - elif saveChanges == wx.ID_YES: - self.canvasSave() - self.SetCursor(wx.Cursor(wx.CURSOR_WAIT)) - if newCanvasSize == None: - newCanvasSize = (100, 30) - self.panelCanvas.canvasImportStore.importNew(newCanvasSize) - self.canvasPathName = None - self.SetCursor(wx.Cursor(wx.NullCursor)) - self._updateStatusBar(); self.onCanvasUpdate(); - # }}} - # {{{ canvasOpen(self): XXX - def canvasOpen(self): - if self.canvasPathName != None: - saveChanges = self._dialogSaveChanges() - if saveChanges == wx.ID_CANCEL: - return - elif saveChanges == wx.ID_NO: - pass - elif saveChanges == wx.ID_YES: - self.canvasSave() - with wx.FileDialog(self, self.CID_OPEN[2], os.getcwd(), "", \ - "*.txt", wx.FD_OPEN) as dialog: - if dialog.ShowModal() == wx.ID_CANCEL: - return False - else: - self.canvasPathName = dialog.GetPath() - self.SetCursor(wx.Cursor(wx.CURSOR_WAIT)) - self.panelCanvas.canvasImportStore.importTextFile(self.canvasPathName) - self.panelCanvas.canvasImportStore.importIntoPanel() - self.SetCursor(wx.Cursor(wx.NullCursor)) - self._updateStatusBar(); self.onCanvasUpdate(); - return True - # }}} - # {{{ canvasSave(self): XXX - def canvasSave(self): - if self.canvasPathName == None: - if self.canvasSaveAs() == False: - return - try: - with open(self.canvasPathName, "w") as outFile: - self.SetCursor(wx.Cursor(wx.CURSOR_WAIT)) - self.panelCanvas.canvasExportStore.exportTextFile( \ - self.panelCanvas.canvasMap, \ - self.panelCanvas.canvasSize, outFile) - self.SetCursor(wx.Cursor(wx.NullCursor)) - return True - except IOError as error: - return False - # }}} - # {{{ canvasSaveAs(self): XXX - def canvasSaveAs(self): - with wx.FileDialog(self, self.CID_SAVEAS[2], os.getcwd(), "", \ - "*.txt", wx.FD_SAVE|wx.FD_OVERWRITE_PROMPT) as dialog: - if dialog.ShowModal() == wx.ID_CANCEL: - return False - else: - self.canvasPathName = dialog.GetPath() - return self.canvasSave() - # }}} - # {{{ onCanvasMotion(self, event): XXX - def onCanvasMotion(self, event, atPoint=None): - eventType = event.GetEventType() - if eventType == wx.wxEVT_ENTER_WINDOW: - self.SetFocus() - elif eventType == wx.wxEVT_MOTION: - self._updateStatusBar(showPos=atPoint) - elif eventType == wx.wxEVT_LEAVE_WINDOW: - pass - # }}} - # {{{ onCanvasUpdate(self): XXX - def onCanvasUpdate(self): + # {{{ onUndoUpdate(self): XXX + def onUndoUpdate(self): if self.panelCanvas.canvasJournal.patchesUndo[self.panelCanvas.canvasJournal.patchesUndoLevel] != None: self.menuItemsById[self.CID_UNDO[0]].Enable(True) self.toolBar.EnableTool(self.CID_UNDO[0], True) @@ -325,106 +190,14 @@ class MiRCARTFrame(MiRCARTGeneralFrame): self.menuItemsById[self.CID_REDO[0]].Enable(False) self.toolBar.EnableTool(self.CID_REDO[0], False) # }}} - # {{{ onFrameCommand(self, event): XXX - def onFrameCommand(self, event): - cid = event.GetId() - if cid == self.CID_NEW[0]: - self.canvasNew() - elif cid == self.CID_OPEN[0]: - self.canvasOpen() - elif cid == self.CID_SAVE[0]: - self.canvasSave() - elif cid == self.CID_SAVEAS[0]: - self.canvasSaveAs() - elif cid == self.CID_EXPORT_AS_PNG[0]: - self.canvasExportAsPng() - elif cid == self.CID_EXPORT_IMGUR[0]: - self.canvasExportImgur() - elif cid == self.CID_EXPORT_PASTEBIN[0]: - self.canvasExportPastebin() - elif cid == self.CID_EXIT[0]: - self.Close(True) - elif cid == self.CID_UNDO[0]: - self.panelCanvas.popUndo() - elif cid == self.CID_REDO[0]: - self.panelCanvas.popRedo() - elif cid == self.CID_CUT[0]: - pass - elif cid == self.CID_COPY[0]: - pass - elif cid == self.CID_PASTE[0]: - pass - elif cid == self.CID_DELETE[0]: - pass - elif cid == self.CID_INCR_CANVAS[0] \ - or cid == self.CID_DECR_CANVAS[0]: - eventDc = self.panelCanvas.canvasBackend.getDeviceContext(self) - if cid == self.CID_INCR_CANVAS[0]: - newCanvasSize = [a+1 for a in self.panelCanvas.canvasSize] - else: - newCanvasSize = [a-1 if a > 1 else a for a in self.panelCanvas.canvasSize] - self.panelCanvas.resize(newCanvasSize) - self.panelCanvas.canvasBackend.resize( \ - newCanvasSize, \ - self.panelCanvas.canvasBackend.cellSize) - for numRow in range(self.panelCanvas.canvasSize[1] - 1): - self.panelCanvas.canvasMap.append([[1, 1], 0, " "]) - self.panelCanvas.canvasMap.append([]) - for numCol in range(self.panelCanvas.canvasSize[0]): - self.panelCanvas.canvasMap[-1].append([[1, 1], 0, " "]) - self.panelCanvas.canvasBackend.drawPatch(eventDc, \ - ([numCol, self.panelCanvas.canvasSize[1] - 1], *[[1, 1], 0, " "])) - wx.SafeYield() - elif cid == self.CID_INCR_BRUSH[0]: - self.panelCanvas.brushSize = \ - [a+1 for a in self.panelCanvas.brushSize] - elif cid == self.CID_DECR_BRUSH[0] \ - and self.panelCanvas.brushSize[0] > 1 \ - and self.panelCanvas.brushSize[1] > 1: - self.panelCanvas.brushSize = \ - [a-1 for a in self.panelCanvas.brushSize] - elif cid == self.CID_SOLID_BRUSH[0]: - pass - elif cid == self.CID_RECT[0]: - self.menuItemsById[cid].Check(True) - self.panelCanvas.canvasCurTool = \ - MiRCARTToolRect(self.panelCanvas) - elif cid == self.CID_CIRCLE[0]: - self.menuItemsById[cid].Check(True) - self.panelCanvas.canvasCurTool = \ - MiRCARTToolCircle(self.panelCanvas) - elif cid == self.CID_LINE[0]: - self.menuItemsById[cid].Check(True) - self.panelCanvas.canvasCurTool = \ - MiRCARTToolLine(self.panelCanvas) - elif cid == self.CID_TEXT[0]: - self.menuItemsById[cid].Check(True) - self.panelCanvas.canvasCurTool = \ - MiRCARTToolText(self.panelCanvas) - elif cid >= self.CID_COLOUR00[0] \ - and cid <= self.CID_COLOUR15[0]: - numColour = cid - self.CID_COLOUR00[0] - if event.GetEventType() == wx.wxEVT_TOOL: - self.panelCanvas.brushColours[0] = numColour - elif event.GetEventType() == wx.wxEVT_TOOL_RCLICKED: - self.panelCanvas.brushColours[1] = numColour - self._updateStatusBar() - # }}} - # {{{ __del__(self): destructor method - def __del__(self): - if self.panelCanvas != None: - self.panelCanvas.Close(); self.panelCanvas = None; - # }}} # # __init__(self, parent, appSize=(840, 630), canvasPos=(25, 50), canvasSize=(125, 35), cellSize=(7, 14)): initialisation method def __init__(self, parent, appSize=(840, 630), canvasPos=(25, 50), canvasSize=(125, 35), cellSize=(7, 14)): self._initPaletteToolBitmaps() panelSkin = super().__init__(parent, wx.ID_ANY, "MiRCART", size=appSize) - self.canvasPathName = None self.panelCanvas = MiRCARTCanvas(panelSkin, parentFrame=self, \ canvasPos=canvasPos, canvasSize=canvasSize, cellSize=cellSize) - self.panelCanvas.canvasCurTool = MiRCARTToolRect(self.panelCanvas) - self.canvasNew() + self.panelCanvas.canvasInterface.canvasNew(None) # vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/MiRCARTGeneralFrame.py b/MiRCARTGeneralFrame.py index 7274af5..75afc7c 100644 --- a/MiRCARTGeneralFrame.py +++ b/MiRCARTGeneralFrame.py @@ -34,68 +34,77 @@ TID_NOTHING = (0x005) TID_SELECT = (0x006) TID_TOOLBAR = (0x007) +# +# Non-items +NID_MENU_SEP = (0x200, TID_NOTHING) +NID_TOOLBAR_SEP = (0x201, TID_NOTHING) + class MiRCARTGeneralFrame(wx.Frame): """XXX""" - menuItemsById = toolBarItemsById = None + itemsById = menuItemsById = toolBarItemsById = None statusBar = toolBar = None - # {{{ _initAccelTable(self, accelsDescr, handler): XXX - def _initAccelTable(self, accelsDescr, handler): + # {{{ _initAccelTable(self, accelsDescr): XXX + def _initAccelTable(self, accelsDescr): accelTableEntries = [wx.AcceleratorEntry() for n in range(len(accelsDescr[2]))] for numAccel in range(len(accelsDescr[2])): accelDescr = accelsDescr[2][numAccel] if accelDescr[5] != None: + self.itemsById[accelDescr[0]] = accelDescr accelTableEntries[numAccel].Set(*accelDescr[5], accelDescr[0]) - self.Bind(wx.EVT_MENU, handler, id=accelDescr[0]) + self.Bind(wx.EVT_MENU, self.onInput, id=accelDescr[0]) return accelTableEntries # }}} - # {{{ _initMenus(self, menusDescr, handler): XXX - def _initMenus(self, menusDescr, handler): + # {{{ _initMenus(self, menusDescr): XXX + def _initMenus(self, menusDescr): self.menuItemsById = {}; menuBar = wx.MenuBar(); for menuDescr in menusDescr: menuWindow = wx.Menu() for menuItem in menuDescr[4]: - if menuItem == self.NID_MENU_SEP: + if menuItem == NID_MENU_SEP: menuWindow.AppendSeparator() elif menuItem[1] == TID_SELECT: + self.itemsById[menuItem[0]] = menuItem menuItemWindow = menuWindow.AppendRadioItem(menuItem[0], menuItem[3], menuItem[2]) self.menuItemsById[menuItem[0]] = menuItemWindow - self.Bind(wx.EVT_MENU, handler, menuItemWindow) - if len(menuItem) == 7: + self.Bind(wx.EVT_MENU, self.onInput, menuItemWindow) + if menuItem[6] != None: menuItemWindow.Check(menuItem[6]) else: + self.itemsById[menuItem[0]] = menuItem menuItemWindow = menuWindow.Append(menuItem[0], menuItem[3], menuItem[2]) self.menuItemsById[menuItem[0]] = menuItemWindow - self.Bind(wx.EVT_MENU, handler, menuItemWindow) - if len(menuItem) == 7: + self.Bind(wx.EVT_MENU, self.onInput, menuItemWindow) + if menuItem[6] != None: menuItemWindow.Enable(menuItem[6]) menuBar.Append(menuWindow, menuDescr[3]) return menuBar # }}} - # {{{ _initToolBars(self, toolBarsDescr, handler): XXX - def _initToolBars(self, toolBarsDescr, handler, panelSkin): + # {{{ _initToolBars(self, toolBarsDescr, panelSkin): XXX + def _initToolBars(self, toolBarsDescr, panelSkin): self.toolBarItemsById = {} self.toolBar = wx.ToolBar(panelSkin, -1, \ style=wx.HORIZONTAL|wx.TB_FLAT|wx.TB_NODIVIDER) self.toolBar.SetToolBitmapSize((16,16)) for toolBarItem in toolBarsDescr[2]: - if toolBarItem == self.NID_TOOLBAR_SEP: + if toolBarItem == NID_TOOLBAR_SEP: self.toolBar.AddSeparator() else: + self.itemsById[toolBarItem[0]] = toolBarItem toolBarItemWindow = self.toolBar.AddTool( \ toolBarItem[0], toolBarItem[2], toolBarItem[4][2]) self.toolBarItemsById[toolBarItem[0]] = toolBarItemWindow - if len(toolBarItem) == 7 \ + if toolBarItem[6] != None \ and toolBarItem[1] == TID_COMMAND: toolBarItemWindow.Enable(toolBarItem[6]) - self.Bind(wx.EVT_TOOL, handler, toolBarItemWindow) - self.Bind(wx.EVT_TOOL_RCLICKED, handler, toolBarItemWindow) + self.Bind(wx.EVT_TOOL, self.onInput, toolBarItemWindow) + self.Bind(wx.EVT_TOOL_RCLICKED, self.onInput, toolBarItemWindow) self.toolBar.Realize(); self.toolBar.Fit(); # }}} - # {{{ _initToolBitmaps(self): XXX + # {{{ _initToolBitmaps(self, toolBarsDescr): XXX def _initToolBitmaps(self, toolBarsDescr): for toolBarItem in toolBarsDescr[2]: - if toolBarItem == self.NID_TOOLBAR_SEP: + if toolBarItem == NID_TOOLBAR_SEP: continue elif toolBarItem[4] == None: toolBarItem[4] = ["", None, wx.ArtProvider.GetBitmap( \ @@ -115,42 +124,32 @@ class MiRCARTGeneralFrame(wx.Frame): toolBitmap.LoadFile(toolBitmapPathName, wx.BITMAP_TYPE_ANY) toolBarItem[4] = ["", None, toolBitmap] # }}} - # {{{ onClose(self, event): XXX - def onClose(self, event): - self.Destroy(); self.__del__(); - # }}} - # {{{ onFrameCommand(self, event): XXX - def onFrameCommand(self, event): + # {{{ onInput(self, event): XXX + def onInput(self, event): pass # }}} # # __init__(self, *args, **kwargs): initialisation method def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + super().__init__(*args, **kwargs); self.itemsById = {}; panelSkin = wx.Panel(self, wx.ID_ANY) # Initialise menu bar, menus & menu items # Initialise toolbar & toolbar items - menuBar = self._initMenus(self.LID_MENUS[2], \ - self.onFrameCommand) + menuBar = self._initMenus(self.LID_MENUS[2]) self.SetMenuBar(menuBar) self._initToolBitmaps(self.LID_TOOLBARS[2]) - toolBar = self._initToolBars(self.LID_TOOLBARS[2], \ - self.onFrameCommand, panelSkin) + toolBar = self._initToolBars(self.LID_TOOLBARS[2], panelSkin) # Initialise accelerators (hotkeys) accelTable = wx.AcceleratorTable( \ - self._initAccelTable(self.LID_ACCELS[2], \ - self.onFrameCommand)) + self._initAccelTable(self.LID_ACCELS[2])) self.SetAcceleratorTable(accelTable) # Initialise status bar self.statusBar = self.CreateStatusBar() - # Bind event handlers - self.Bind(wx.EVT_CLOSE, self.onClose) - # Set focus on & show window self.SetFocus(); self.Show(True); From f9f910160f8b999f8bc1d663efd9dafa4ea915d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Wed, 10 Jan 2018 04:23:54 +0100 Subject: [PATCH 097/148] MiRCART{Canvas{,Interface},Frame}.py: merge on{StatusBar,Undo}Update() into onCanvasUpdate(). MiRCARTCanvas{Frame,Interface}.py: adds canvas{De,In}crCanvas{Height,Width}() & CID_*. MiRCARTCanvasFrame.py:onCanvasUpdate(): updated. --- MiRCARTCanvas.py | 10 +-- MiRCARTCanvasInterface.py | 42 ++++++++---- MiRCARTFrame.py | 132 +++++++++++++++++++++----------------- 3 files changed, 107 insertions(+), 77 deletions(-) diff --git a/MiRCARTCanvas.py b/MiRCARTCanvas.py index 5de4b44..6a3f73e 100644 --- a/MiRCARTCanvas.py +++ b/MiRCARTCanvas.py @@ -48,7 +48,7 @@ class MiRCARTCanvas(wx.Panel): for patch in deltaPatches: if self.canvasBackend.drawPatch(eventDc, patch): self._commitPatch(patch) - self.parentFrame.onUndoUpdate() + self.parentFrame.onCanvasUpdate(newUndoLevel=self.canvasJournal.patchesUndoLevel) # }}} # {{{ _dispatchPatch(self, eventDc, isCursor, patch): XXX def _dispatchPatch(self, eventDc, isCursor, patch): @@ -98,9 +98,10 @@ class MiRCARTCanvas(wx.Panel): event.Dragging(), event.LeftIsDown(), event.RightIsDown(), \ self._dispatchPatch, eventDc) if self._canvasDirty: - self.parentFrame.onUndoUpdate() + self.parentFrame.onCanvasUpdate(newCellPos=self.brushPos, \ + newUndoLevel=self.canvasJournal.patchesUndoLevel) if eventType == wx.wxEVT_MOTION: - self.parentFrame.onStatusBarUpdate(showPos=mapPoint) + self.parentFrame.onCanvasUpdate(newCellPos=mapPoint) # }}} # {{{ onPanelLeaveWindow(self, event): XXX def onPanelLeaveWindow(self, event): @@ -149,7 +150,7 @@ class MiRCARTCanvas(wx.Panel): self.canvasBackend.cellSize)]) self.canvasBackend.reset(self.canvasSize, self.canvasBackend.cellSize) self.canvasJournal.resetCursor(); self.canvasJournal.resetUndo(); - self.parentFrame.onUndoUpdate() + self.parentFrame.onCanvasUpdate(newUndoLevel=-1) # }}} # @@ -161,6 +162,7 @@ class MiRCARTCanvas(wx.Panel): self.parentFrame = parentFrame self.canvasMap = None; self.canvasPos = canvasPos; self.canvasSize = canvasSize; self.brushColours = [4, 1]; self.brushPos = [0, 0]; self.brushSize = [1, 1]; + self.parentFrame.onCanvasUpdate(newColours=self.brushColours) self.canvasBackend = MiRCARTCanvasBackend(canvasSize, cellSize) self.canvasJournal = MiRCARTCanvasJournal() self.canvasExportStore = MiRCARTCanvasExportStore(parentCanvas=self) diff --git a/MiRCARTCanvasInterface.py b/MiRCARTCanvasInterface.py index fc30fec..d0b2926 100644 --- a/MiRCARTCanvasInterface.py +++ b/MiRCARTCanvasInterface.py @@ -69,7 +69,7 @@ class MiRCARTCanvasInterface(): self.parentCanvas.brushColours[0] = numColour elif event.GetEventType() == wx.wxEVT_TOOL_RCLICKED: self.parentCanvas.brushColours[1] = numColour - self.parentFrame.onStatusBarUpdate() + self.parentFrame.onCanvasUpdate(newColours=self.parentCanvas.brushColours) # }}} # {{{ canvasCopy(self, event): XXX def canvasCopy(self, event): @@ -86,10 +86,19 @@ class MiRCARTCanvasInterface(): self.parentCanvas.brushSize = \ [a-1 for a in self.parentCanvas.brushSize] # }}} - # {{{ canvasDecrCanvas(self, event): XXX - def canvasDecrCanvas(self, event): - newCanvasSize = [a-1 if a > 1 else a for a in self.parentCanvas.canvasSize] - self._updateCanvasSize(newCanvasSize) + # {{{ canvasDecrCanvasHeight(self, event): XXX + def canvasDecrCanvasHeight(self, event): + if self.parentCanvas.canvasSize[1] > 1: + self._updateCanvasSize([ \ + self.parentCanvas.canvasSize[0], \ + self.parentCanvas.canvasSize[1]-1]) + # }}} + # {{{ canvasDecrCanvasWidth(self, event): XXX + def canvasDecrCanvasWidth(self, event): + if self.parentCanvas.canvasSize[0] > 1: + self._updateCanvasSize([ \ + self.parentCanvas.canvasSize[0]-1, \ + self.parentCanvas.canvasSize[1]]) # }}} # {{{ canvasDelete(self, event): XXX def canvasDelete(self, event): @@ -157,10 +166,17 @@ class MiRCARTCanvasInterface(): self.parentCanvas.brushSize = \ [a+1 for a in self.parentCanvas.brushSize] # }}} - # {{{ canvasIncrCanvas(self, event): XXX - def canvasIncrCanvas(self, event): - newCanvasSize = [a+1 for a in self.parentCanvas.canvasSize] - self._updateCanvasSize(newCanvasSize) + # {{{ canvasIncrCanvasHeight(self, event): XXX + def canvasIncrCanvasHeight(self, event): + self._updateCanvasSize([ \ + self.parentCanvas.canvasSize[0], \ + self.parentCanvas.canvasSize[1]+1]) + # }}} + # {{{ canvasIncrCanvasWidth(self, event): XXX + def canvasIncrCanvasWidth(self, event): + self._updateCanvasSize([ \ + self.parentCanvas.canvasSize[0]+1, \ + self.parentCanvas.canvasSize[1]]) # }}} # {{{ canvasNew(self, event, newCanvasSize=None): XXX def canvasNew(self, event, newCanvasSize=None): @@ -178,8 +194,8 @@ class MiRCARTCanvasInterface(): self.parentCanvas.canvasImportStore.importNew(newCanvasSize) self.canvasPathName = None self.parentCanvas.SetCursor(wx.Cursor(wx.NullCursor)) - self.parentFrame.onStatusBarUpdate() - self.parentFrame.onUndoUpdate() + self.parentFrame.onCanvasUpdate( \ + newPathName="", newUndoLevel=-1) # }}} # {{{ canvasOpen(self, event): XXX def canvasOpen(self, event): @@ -201,8 +217,8 @@ class MiRCARTCanvasInterface(): self.parentCanvas.canvasImportStore.importTextFile(self.canvasPathName) self.parentCanvas.canvasImportStore.importIntoPanel() self.parentCanvas.SetCursor(wx.Cursor(wx.NullCursor)) - self.parentFrame.onStatusBarUpdate(showFileName=self.canvasPathName) - self.parentFrame.onUndoUpdate() + self.parentFrame.onCanvasUpdate( \ + newPathName=self.canvasPathName, newUndoLevel=-1) return True # }}} # {{{ canvasPaste(self, event): XXX diff --git a/MiRCARTFrame.py b/MiRCARTFrame.py index 1d7b3be..4a76177 100644 --- a/MiRCARTFrame.py +++ b/MiRCARTFrame.py @@ -34,6 +34,7 @@ import os, wx class MiRCARTFrame(MiRCARTGeneralFrame): """XXX""" panelCanvas = None + lastCellPos = lastColours = lastPathName = lastUndoLevel = None # {{{ Commands # Id Type Id Labels Icon bitmap Accelerator [Initial state] @@ -54,15 +55,19 @@ class MiRCARTFrame(MiRCARTGeneralFrame): CID_COPY = [0x10b, TID_COMMAND, "Copy", "&Copy", ["", wx.ART_COPY], None, False, MiRCARTCanvasInterface.canvasCopy] CID_PASTE = [0x10c, TID_COMMAND, "Paste", "&Paste", ["", wx.ART_PASTE], None, False, MiRCARTCanvasInterface.canvasPaste] CID_DELETE = [0x10d, TID_COMMAND, "Delete", "De&lete", ["", wx.ART_DELETE], None, False, MiRCARTCanvasInterface.canvasDelete] - CID_INCR_CANVAS = [0x10e, TID_COMMAND, "Increase canvas size", \ - "I&ncrease canvas size", ["", wx.ART_PLUS], [wx.ACCEL_ALT, ord("+")], None, MiRCARTCanvasInterface.canvasIncrCanvas] - CID_DECR_CANVAS = [0x10f, TID_COMMAND, "Decrease canvas size", \ - "D&ecrease canvas size", ["", wx.ART_MINUS], [wx.ACCEL_ALT, ord("-")], None, MiRCARTCanvasInterface.canvasDecrCanvas] - CID_INCR_BRUSH = [0x110, TID_COMMAND, "Increase brush size", \ - "&Increase brush size", ["", wx.ART_PLUS], [wx.ACCEL_CTRL, ord("+")], None, MiRCARTCanvasInterface.canvasIncrBrush] - CID_DECR_BRUSH = [0x111, TID_COMMAND, "Decrease brush size", \ + CID_INCRW_CANVAS = [0x10e, TID_COMMAND, "Increase canvas width", \ + "Increase canvas &width", ["", wx.ART_PLUS], [wx.ACCEL_ALT, ord("D")], None, MiRCARTCanvasInterface.canvasIncrCanvasWidth] + CID_DECRW_CANVAS = [0x10f, TID_COMMAND, "Decrease canvas width", \ + "Decrease canvas w&idth", ["", wx.ART_MINUS], [wx.ACCEL_ALT, ord("A")], None, MiRCARTCanvasInterface.canvasDecrCanvasWidth] + CID_INCRH_CANVAS = [0x110, TID_COMMAND, "Increase canvas height", \ + "Increase canvas &height", ["", wx.ART_PLUS], [wx.ACCEL_ALT, ord("S")], None, MiRCARTCanvasInterface.canvasIncrCanvasHeight] + CID_DECRH_CANVAS = [0x111, TID_COMMAND, "Decrease canvas height", \ + "Decrease canvas h&eight", ["", wx.ART_MINUS], [wx.ACCEL_ALT, ord("W")], None, MiRCARTCanvasInterface.canvasDecrCanvasHeight] + CID_INCR_BRUSH = [0x112, TID_COMMAND, "Increase brush size", \ + "I&ncrease brush size", ["", wx.ART_PLUS], [wx.ACCEL_CTRL, ord("+")], None, MiRCARTCanvasInterface.canvasIncrBrush] + CID_DECR_BRUSH = [0x113, TID_COMMAND, "Decrease brush size", \ "&Decrease brush size", ["", wx.ART_MINUS], [wx.ACCEL_CTRL, ord("-")], None, MiRCARTCanvasInterface.canvasDecrBrush] - CID_SOLID_BRUSH = [0x112, TID_SELECT, "Solid brush", "&Solid brush", None, None, True, MiRCARTCanvasInterface.canvasBrushSolid] + CID_SOLID_BRUSH = [0x114, TID_SELECT, "Solid brush", "&Solid brush", None, None, True, MiRCARTCanvasInterface.canvasBrushSolid] CID_RECT = [0x150, TID_SELECT, "Rectangle", "&Rectangle", ["toolRect.png"], [wx.ACCEL_CTRL, ord("R")], True, MiRCARTCanvasInterface.canvasToolRect] CID_CIRCLE = [0x151, TID_SELECT, "Circle", "&Circle", ["toolCircle.png"], [wx.ACCEL_CTRL, ord("C")], False, MiRCARTCanvasInterface.canvasToolCircle] @@ -87,34 +92,35 @@ class MiRCARTFrame(MiRCARTGeneralFrame): CID_COLOUR15 = [0x1af, TID_COMMAND, "Colour #15", "Colour #15", None, None, None, MiRCARTCanvasInterface.canvasColour] # }}} # {{{ Menus - MID_FILE = (0x300, TID_MENU, "File", "&File", ( \ - CID_NEW, CID_OPEN, CID_SAVE, CID_SAVEAS, NID_MENU_SEP, \ - CID_EXPORT_AS_PNG, CID_EXPORT_IMGUR, CID_EXPORT_PASTEBIN, NID_MENU_SEP, \ + MID_FILE = (0x300, TID_MENU, "File", "&File", ( \ + CID_NEW, CID_OPEN, CID_SAVE, CID_SAVEAS, NID_MENU_SEP, \ + CID_EXPORT_AS_PNG, CID_EXPORT_IMGUR, CID_EXPORT_PASTEBIN, NID_MENU_SEP, \ CID_EXIT)) - MID_EDIT = (0x301, TID_MENU, "Edit", "&Edit", ( \ - CID_UNDO, CID_REDO, NID_MENU_SEP, \ - CID_CUT, CID_COPY, CID_PASTE, CID_DELETE, NID_MENU_SEP, \ - CID_INCR_CANVAS, CID_DECR_CANVAS, NID_MENU_SEP, \ + MID_EDIT = (0x301, TID_MENU, "Edit", "&Edit", ( \ + CID_UNDO, CID_REDO, NID_MENU_SEP, \ + CID_CUT, CID_COPY, CID_PASTE, CID_DELETE, NID_MENU_SEP, \ + CID_INCRW_CANVAS, CID_DECRW_CANVAS, CID_INCRH_CANVAS, CID_DECRH_CANVAS, NID_MENU_SEP, \ CID_INCR_BRUSH, CID_DECR_BRUSH, CID_SOLID_BRUSH)) - MID_TOOLS = (0x302, TID_MENU, "Tools", "&Tools", ( \ + MID_TOOLS = (0x302, TID_MENU, "Tools", "&Tools", ( \ CID_RECT, CID_CIRCLE, CID_LINE, CID_TEXT)) # }}} # {{{ Toolbars - BID_TOOLBAR = (0x400, TID_TOOLBAR, ( \ - CID_NEW, CID_OPEN, CID_SAVE, CID_SAVEAS, NID_TOOLBAR_SEP, \ - CID_UNDO, CID_REDO, NID_TOOLBAR_SEP, \ - CID_CUT, CID_COPY, CID_PASTE, CID_DELETE, NID_TOOLBAR_SEP, \ - CID_INCR_BRUSH, CID_DECR_BRUSH, NID_TOOLBAR_SEP, \ - CID_RECT, CID_CIRCLE, CID_LINE, CID_TEXT, NID_TOOLBAR_SEP, \ - CID_COLOUR00, CID_COLOUR01, CID_COLOUR02, CID_COLOUR03, CID_COLOUR04, \ - CID_COLOUR05, CID_COLOUR06, CID_COLOUR07, CID_COLOUR08, CID_COLOUR09, \ - CID_COLOUR10, CID_COLOUR11, CID_COLOUR12, CID_COLOUR13, CID_COLOUR14, \ + BID_TOOLBAR = (0x400, TID_TOOLBAR, ( \ + CID_NEW, CID_OPEN, CID_SAVE, CID_SAVEAS, NID_TOOLBAR_SEP, \ + CID_UNDO, CID_REDO, NID_TOOLBAR_SEP, \ + CID_CUT, CID_COPY, CID_PASTE, CID_DELETE, NID_TOOLBAR_SEP, \ + CID_INCR_BRUSH, CID_DECR_BRUSH, NID_TOOLBAR_SEP, \ + CID_RECT, CID_CIRCLE, CID_LINE, CID_TEXT, NID_TOOLBAR_SEP, \ + CID_COLOUR00, CID_COLOUR01, CID_COLOUR02, CID_COLOUR03, CID_COLOUR04, \ + CID_COLOUR05, CID_COLOUR06, CID_COLOUR07, CID_COLOUR08, CID_COLOUR09, \ + CID_COLOUR10, CID_COLOUR11, CID_COLOUR12, CID_COLOUR13, CID_COLOUR14, \ CID_COLOUR15)) # }}} # {{{ Accelerators (hotkeys) - AID_EDIT = (0x500, TID_ACCELS, ( \ - CID_NEW, CID_OPEN, CID_SAVE, CID_UNDO, CID_REDO, \ - CID_INCR_CANVAS, CID_DECR_CANVAS, CID_INCR_BRUSH, CID_DECR_BRUSH)) + AID_EDIT = (0x500, TID_ACCELS, ( \ + CID_NEW, CID_OPEN, CID_SAVE, CID_UNDO, CID_REDO, \ + CID_INCRW_CANVAS, CID_DECRW_CANVAS, CID_INCRH_CANVAS, CID_DECRH_CANVAS, \ + CID_INCR_BRUSH, CID_DECR_BRUSH)) # }}} # {{{ Lists LID_ACCELS = (0x600, TID_LIST, (AID_EDIT)) @@ -152,43 +158,49 @@ class MiRCARTFrame(MiRCARTGeneralFrame): else: self.itemsById[eventId][7](self.panelCanvas.canvasInterface, event) # }}} - # {{{ onStatusBarUpdate(self, showColours=None, showFileName=True, showPos=None): XXX - def onStatusBarUpdate(self, showColours=True, showFileName=True, showPos=True): - if showColours == True: - showColours = self.panelCanvas.brushColours - if showPos == True: - showPos = self.panelCanvas.brushPos - if showFileName == True: - showFileName = self.panelCanvas.canvasInterface.canvasPathName + # {{{ onCanvasUpdate(self, newCellPos=None, newColours=None, newPathName=None, newUndoLevel=None): XXX + def onCanvasUpdate(self, newCellPos=None, newColours=None, newPathName=None, newUndoLevel=None): + if newCellPos != None: + self.lastCellPos = newCellPos + if newColours != None: + self.lastColours = newColours + if newPathName != None: + self.lastPathName = newPathName + if newUndoLevel != None: + self.lastUndoLevel = newUndoLevel textItems = [] - if showPos != None: + if self.lastCellPos != None: textItems.append("X: {:03d} Y: {:03d}".format( \ - showPos[0], showPos[1])) - if showColours != None: + *self.lastCellPos)) + if self.lastColours != None: textItems.append("FG: {:02d}, BG: {:02d}".format( \ - showColours[0],showColours[1])) + *self.lastColours)) textItems.append("{} on {}".format( \ - MiRCARTColours[showColours[0]][4], \ - MiRCARTColours[showColours[1]][4])) - if showFileName != None: - textItems.append("Current file: {}".format( \ - os.path.basename(showFileName))) + MiRCARTColours[self.lastColours[0]][4], \ + MiRCARTColours[self.lastColours[1]][4])) + if self.lastPathName != None: + if self.lastPathName != "": + basePathName = os.path.basename(self.lastPathName) + textItems.append("Current file: {}".format(basePathName)) + self.SetTitle("{} - MiRCART".format(basePathName)) + else: + self.SetTitle("MiRCART") + if self.lastUndoLevel != None: + textItems.append("Undo level: {}".format(self.lastUndoLevel)) self.statusBar.SetStatusText(" | ".join(textItems)) - # }}} - # {{{ onUndoUpdate(self): XXX - def onUndoUpdate(self): - if self.panelCanvas.canvasJournal.patchesUndo[self.panelCanvas.canvasJournal.patchesUndoLevel] != None: - self.menuItemsById[self.CID_UNDO[0]].Enable(True) - self.toolBar.EnableTool(self.CID_UNDO[0], True) - else: - self.menuItemsById[self.CID_UNDO[0]].Enable(False) - self.toolBar.EnableTool(self.CID_UNDO[0], False) - if self.panelCanvas.canvasJournal.patchesUndoLevel > 0: - self.menuItemsById[self.CID_REDO[0]].Enable(True) - self.toolBar.EnableTool(self.CID_REDO[0], True) - else: - self.menuItemsById[self.CID_REDO[0]].Enable(False) - self.toolBar.EnableTool(self.CID_REDO[0], False) + if self.lastUndoLevel != None: + if self.lastUndoLevel >= 0: + self.menuItemsById[self.CID_UNDO[0]].Enable(True) + self.toolBar.EnableTool(self.CID_UNDO[0], True) + else: + self.menuItemsById[self.CID_UNDO[0]].Enable(False) + self.toolBar.EnableTool(self.CID_UNDO[0], False) + if self.lastUndoLevel > 0: + self.menuItemsById[self.CID_REDO[0]].Enable(True) + self.toolBar.EnableTool(self.CID_REDO[0], True) + else: + self.menuItemsById[self.CID_REDO[0]].Enable(False) + self.toolBar.EnableTool(self.CID_REDO[0], False) # }}} # From 12027bc6b590912a0477d046653a5d1384d85ad6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Wed, 10 Jan 2018 04:31:20 +0100 Subject: [PATCH 098/148] MiRCARTToolText.py: fix non-US ASCII character handling. --- MiRCARTToolText.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/MiRCARTToolText.py b/MiRCARTToolText.py index ca1386a..ebb58fb 100644 --- a/MiRCARTToolText.py +++ b/MiRCARTToolText.py @@ -23,7 +23,7 @@ # from MiRCARTTool import MiRCARTTool -import string, wx +import wx class MiRCARTToolText(MiRCARTTool): """XXX""" @@ -36,8 +36,6 @@ class MiRCARTToolText(MiRCARTTool): if keyModifiers != wx.MOD_NONE \ and keyModifiers != wx.MOD_SHIFT: return True - elif not keyChar in string.printable: - return True else: if self.textColours == None: self.textColours = brushColours.copy() From e157f479ceee7f1a5999e4712f13814cf7ff793a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Wed, 10 Jan 2018 04:36:39 +0100 Subject: [PATCH 099/148] MiRCART{Canvas,Frame}.py: include canvas width and height in status bar. --- MiRCARTCanvas.py | 3 ++- MiRCARTFrame.py | 10 +++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/MiRCARTCanvas.py b/MiRCARTCanvas.py index 6a3f73e..a00e5d7 100644 --- a/MiRCARTCanvas.py +++ b/MiRCARTCanvas.py @@ -150,7 +150,8 @@ class MiRCARTCanvas(wx.Panel): self.canvasBackend.cellSize)]) self.canvasBackend.reset(self.canvasSize, self.canvasBackend.cellSize) self.canvasJournal.resetCursor(); self.canvasJournal.resetUndo(); - self.parentFrame.onCanvasUpdate(newUndoLevel=-1) + self.parentFrame.onCanvasUpdate( \ + newSize=self.canvasSize, newUndoLevel=-1) # }}} # diff --git a/MiRCARTFrame.py b/MiRCARTFrame.py index 4a76177..3cf26e9 100644 --- a/MiRCARTFrame.py +++ b/MiRCARTFrame.py @@ -34,7 +34,7 @@ import os, wx class MiRCARTFrame(MiRCARTGeneralFrame): """XXX""" panelCanvas = None - lastCellPos = lastColours = lastPathName = lastUndoLevel = None + lastCellPos = lastColours = lastPathName = lastSize = lastUndoLevel = None # {{{ Commands # Id Type Id Labels Icon bitmap Accelerator [Initial state] @@ -158,20 +158,24 @@ class MiRCARTFrame(MiRCARTGeneralFrame): else: self.itemsById[eventId][7](self.panelCanvas.canvasInterface, event) # }}} - # {{{ onCanvasUpdate(self, newCellPos=None, newColours=None, newPathName=None, newUndoLevel=None): XXX - def onCanvasUpdate(self, newCellPos=None, newColours=None, newPathName=None, newUndoLevel=None): + # {{{ onCanvasUpdate(self, newCellPos=None, newColours=None, newPathName=None, newSize=None, newUndoLevel=None): XXX + def onCanvasUpdate(self, newCellPos=None, newColours=None, newPathName=None, newSize=None, newUndoLevel=None): if newCellPos != None: self.lastCellPos = newCellPos if newColours != None: self.lastColours = newColours if newPathName != None: self.lastPathName = newPathName + if newSize != None: + self.lastSize = newSize if newUndoLevel != None: self.lastUndoLevel = newUndoLevel textItems = [] if self.lastCellPos != None: textItems.append("X: {:03d} Y: {:03d}".format( \ *self.lastCellPos)) + if self.lastSize != None: + textItems.append("W: {:03d} H: {:03d}".format(*self.lastSize)) if self.lastColours != None: textItems.append("FG: {:02d}, BG: {:02d}".format( \ *self.lastColours)) From 3fa377791ab233f3e2646f800d4b2c70d030e4d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Wed, 10 Jan 2018 04:44:13 +0100 Subject: [PATCH 100/148] MiRCARTToolLine.py: honour brush size. --- MiRCARTToolLine.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/MiRCARTToolLine.py b/MiRCARTToolLine.py index 3180d0d..aeabc43 100644 --- a/MiRCARTToolLine.py +++ b/MiRCARTToolLine.py @@ -39,8 +39,8 @@ class MiRCARTToolLine(MiRCARTTool): def _pointSwap(self, a, b): return [b, a] # }}} - # {{{ _getLine(self, brushColours, eventDc, isCursor, originPoint, targetPoint, dispatchFn): XXX - def _getLine(self, brushColours, eventDc, isCursor, originPoint, targetPoint, dispatchFn): + # {{{ _getLine(self, brushColours, brushSize, eventDc, isCursor, originPoint, targetPoint, dispatchFn): XXX + def _getLine(self, brushColours, brushSize, eventDc, isCursor, originPoint, targetPoint, dispatchFn): originPoint = originPoint.copy(); targetPoint = targetPoint.copy(); pointDelta = self._pointDelta(originPoint, targetPoint) lineXSign = 1 if pointDelta[0] > 0 else -1; @@ -53,14 +53,15 @@ class MiRCARTToolLine(MiRCARTTool): lineXX, lineXY, lineYX, lineYY = 0, lineYSign, lineXSign, 0 lineD = 2 * pointDelta[1] - pointDelta[0]; lineY = 0; for lineX in range(pointDelta[0] + 1): - patch = [[ \ - originPoint[0] + lineX*lineXX + lineY*lineYX, \ - originPoint[1] + lineX*lineXY + lineY*lineYY], \ - brushColours, 0, " "] - if isCursor: - dispatchFn(eventDc, False, patch); dispatchFn(eventDc, True, patch); - else: - dispatchFn(eventDc, True, patch) + for brushStep in range(brushSize[0]): + patch = [[ \ + originPoint[0] + lineX*lineXX + lineY*lineYX + brushStep, \ + originPoint[1] + lineX*lineXY + lineY*lineYY], \ + brushColours, 0, " "] + if isCursor: + dispatchFn(eventDc, False, patch); dispatchFn(eventDc, True, patch); + else: + dispatchFn(eventDc, True, patch) if lineD > 0: lineD -= pointDelta[0]; lineY += 1; lineD += pointDelta[1] @@ -84,8 +85,8 @@ class MiRCARTToolLine(MiRCARTTool): elif self.toolState == self.TS_ORIGIN: targetPoint = list(atPoint) originPoint = self.toolOriginPoint - self._getLine(brushColours, eventDc, \ - isLeftDown or isRightDown, \ + self._getLine(brushColours, brushSize, \ + eventDc, isLeftDown or isRightDown, \ originPoint, targetPoint, dispatchFn) if isLeftDown or isRightDown: self.toolState = self.TS_NONE From a320c904fe1011890dea8d0b0d99a7b26827e068 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Wed, 10 Jan 2018 05:00:52 +0100 Subject: [PATCH 101/148] MiRCART{Canvas{,Interface},Frame}.py: remove undo level & show brush size in status bar. --- MiRCARTCanvas.py | 3 ++- MiRCARTCanvasInterface.py | 2 ++ MiRCARTFrame.py | 18 +++++++++++------- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/MiRCARTCanvas.py b/MiRCARTCanvas.py index a00e5d7..7c4c67e 100644 --- a/MiRCARTCanvas.py +++ b/MiRCARTCanvas.py @@ -163,7 +163,8 @@ class MiRCARTCanvas(wx.Panel): self.parentFrame = parentFrame self.canvasMap = None; self.canvasPos = canvasPos; self.canvasSize = canvasSize; self.brushColours = [4, 1]; self.brushPos = [0, 0]; self.brushSize = [1, 1]; - self.parentFrame.onCanvasUpdate(newColours=self.brushColours) + self.parentFrame.onCanvasUpdate( \ + newBrushSize=self.brushSize, newColours=self.brushColours) self.canvasBackend = MiRCARTCanvasBackend(canvasSize, cellSize) self.canvasJournal = MiRCARTCanvasJournal() self.canvasExportStore = MiRCARTCanvasExportStore(parentCanvas=self) diff --git a/MiRCARTCanvasInterface.py b/MiRCARTCanvasInterface.py index d0b2926..e11921b 100644 --- a/MiRCARTCanvasInterface.py +++ b/MiRCARTCanvasInterface.py @@ -85,6 +85,7 @@ class MiRCARTCanvasInterface(): and self.parentCanvas.brushSize[1] > 1: self.parentCanvas.brushSize = \ [a-1 for a in self.parentCanvas.brushSize] + self.parentFrame.onCanvasUpdate(newBrushSize=self.parentCanvas.brushSize) # }}} # {{{ canvasDecrCanvasHeight(self, event): XXX def canvasDecrCanvasHeight(self, event): @@ -165,6 +166,7 @@ class MiRCARTCanvasInterface(): def canvasIncrBrush(self, event): self.parentCanvas.brushSize = \ [a+1 for a in self.parentCanvas.brushSize] + self.parentFrame.onCanvasUpdate(newBrushSize=self.parentCanvas.brushSize) # }}} # {{{ canvasIncrCanvasHeight(self, event): XXX def canvasIncrCanvasHeight(self, event): diff --git a/MiRCARTFrame.py b/MiRCARTFrame.py index 3cf26e9..e2278b5 100644 --- a/MiRCARTFrame.py +++ b/MiRCARTFrame.py @@ -34,7 +34,7 @@ import os, wx class MiRCARTFrame(MiRCARTGeneralFrame): """XXX""" panelCanvas = None - lastCellPos = lastColours = lastPathName = lastSize = lastUndoLevel = None + lastBrushSize = lastCellPos = lastColours = lastPathName = lastSize = lastUndoLevel = None # {{{ Commands # Id Type Id Labels Icon bitmap Accelerator [Initial state] @@ -158,8 +158,10 @@ class MiRCARTFrame(MiRCARTGeneralFrame): else: self.itemsById[eventId][7](self.panelCanvas.canvasInterface, event) # }}} - # {{{ onCanvasUpdate(self, newCellPos=None, newColours=None, newPathName=None, newSize=None, newUndoLevel=None): XXX - def onCanvasUpdate(self, newCellPos=None, newColours=None, newPathName=None, newSize=None, newUndoLevel=None): + # {{{ onCanvasUpdate(self, newBrushSize=None, newCellPos=None, newColours=None, newPathName=None, newSize=None, newUndoLevel=None): XXX + def onCanvasUpdate(self, newBrushSize=None, newCellPos=None, newColours=None, newPathName=None, newSize=None, newUndoLevel=None): + if newBrushSize != None: + self.lastBrushSize = newBrushSize if newCellPos != None: self.lastCellPos = newCellPos if newColours != None: @@ -175,22 +177,24 @@ class MiRCARTFrame(MiRCARTGeneralFrame): textItems.append("X: {:03d} Y: {:03d}".format( \ *self.lastCellPos)) if self.lastSize != None: - textItems.append("W: {:03d} H: {:03d}".format(*self.lastSize)) + textItems.append("W: {:03d} H: {:03d}".format( \ + *self.lastSize)) + if self.lastBrushSize != None: + textItems.append("Brush: {:02d}x{:02d}".format( \ + *self.lastBrushSize)) if self.lastColours != None: textItems.append("FG: {:02d}, BG: {:02d}".format( \ *self.lastColours)) textItems.append("{} on {}".format( \ MiRCARTColours[self.lastColours[0]][4], \ MiRCARTColours[self.lastColours[1]][4])) - if self.lastPathName != None: + if self.lastPathName != None: if self.lastPathName != "": basePathName = os.path.basename(self.lastPathName) textItems.append("Current file: {}".format(basePathName)) self.SetTitle("{} - MiRCART".format(basePathName)) else: self.SetTitle("MiRCART") - if self.lastUndoLevel != None: - textItems.append("Undo level: {}".format(self.lastUndoLevel)) self.statusBar.SetStatusText(" | ".join(textItems)) if self.lastUndoLevel != None: if self.lastUndoLevel >= 0: From 402e5421375aa63e2e50cc73370edf745176b61a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Wed, 10 Jan 2018 05:05:59 +0100 Subject: [PATCH 102/148] MiRCART{CanvasInterface,Frame,Tool*}.py: include current tool name in status bar text. --- MiRCARTCanvasInterface.py | 4 ++++ MiRCARTFrame.py | 10 +++++++--- MiRCARTToolCircle.py | 1 + MiRCARTToolLine.py | 1 + MiRCARTToolRect.py | 1 + MiRCARTToolText.py | 1 + 6 files changed, 15 insertions(+), 3 deletions(-) diff --git a/MiRCARTCanvasInterface.py b/MiRCARTCanvasInterface.py index e11921b..2222452 100644 --- a/MiRCARTCanvasInterface.py +++ b/MiRCARTCanvasInterface.py @@ -261,18 +261,22 @@ class MiRCARTCanvasInterface(): # {{{ canvasToolCircle(self, event): XXX def canvasToolCircle(self, event): self.canvasTool = MiRCARTToolCircle(self.parentCanvas) + self.parentFrame.onCanvasUpdate(newToolName=self.canvasTool.name) # }}} # {{{ canvasToolLine(self, event): XXX def canvasToolLine(self, event): self.canvasTool = MiRCARTToolLine(self.parentCanvas) + self.parentFrame.onCanvasUpdate(newToolName=self.canvasTool.name) # }}} # {{{ canvasToolRect(self, event): XXX def canvasToolRect(self, event): self.canvasTool = MiRCARTToolRect(self.parentCanvas) + self.parentFrame.onCanvasUpdate(newToolName=self.canvasTool.name) # }}} # {{{ canvasToolText(self, event): XXX def canvasToolText(self, event): self.canvasTool = MiRCARTToolText(self.parentCanvas) + self.parentFrame.onCanvasUpdate(newToolName=self.canvasTool.name) # }}} # {{{ canvasUndo(self, event): XXX def canvasUndo(self, event): diff --git a/MiRCARTFrame.py b/MiRCARTFrame.py index e2278b5..8a92dbf 100644 --- a/MiRCARTFrame.py +++ b/MiRCARTFrame.py @@ -34,7 +34,7 @@ import os, wx class MiRCARTFrame(MiRCARTGeneralFrame): """XXX""" panelCanvas = None - lastBrushSize = lastCellPos = lastColours = lastPathName = lastSize = lastUndoLevel = None + lastBrushSize = lastCellPos = lastColours = lastPathName = lastSize = lastToolName = lastUndoLevel = None # {{{ Commands # Id Type Id Labels Icon bitmap Accelerator [Initial state] @@ -158,8 +158,8 @@ class MiRCARTFrame(MiRCARTGeneralFrame): else: self.itemsById[eventId][7](self.panelCanvas.canvasInterface, event) # }}} - # {{{ onCanvasUpdate(self, newBrushSize=None, newCellPos=None, newColours=None, newPathName=None, newSize=None, newUndoLevel=None): XXX - def onCanvasUpdate(self, newBrushSize=None, newCellPos=None, newColours=None, newPathName=None, newSize=None, newUndoLevel=None): + # {{{ onCanvasUpdate(self, newBrushSize=None, newCellPos=None, newColours=None, newPathName=None, newSize=None, newToolName=None, newUndoLevel=None): XXX + def onCanvasUpdate(self, newBrushSize=None, newCellPos=None, newColours=None, newPathName=None, newSize=None, newToolName=None, newUndoLevel=None): if newBrushSize != None: self.lastBrushSize = newBrushSize if newCellPos != None: @@ -170,6 +170,8 @@ class MiRCARTFrame(MiRCARTGeneralFrame): self.lastPathName = newPathName if newSize != None: self.lastSize = newSize + if newToolName != None: + self.lastToolName = newToolName if newUndoLevel != None: self.lastUndoLevel = newUndoLevel textItems = [] @@ -195,6 +197,8 @@ class MiRCARTFrame(MiRCARTGeneralFrame): self.SetTitle("{} - MiRCART".format(basePathName)) else: self.SetTitle("MiRCART") + if self.lastToolName != None: + textItems.append("Current tool: {}".format(self.lastToolName)) self.statusBar.SetStatusText(" | ".join(textItems)) if self.lastUndoLevel != None: if self.lastUndoLevel >= 0: diff --git a/MiRCARTToolCircle.py b/MiRCARTToolCircle.py index 52497e3..0fc5fb7 100644 --- a/MiRCARTToolCircle.py +++ b/MiRCARTToolCircle.py @@ -26,6 +26,7 @@ from MiRCARTTool import MiRCARTTool class MiRCARTToolCircle(MiRCARTTool): """XXX""" + name = "Circle" # # onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown, dispatchFn, eventDc): XXX diff --git a/MiRCARTToolLine.py b/MiRCARTToolLine.py index aeabc43..d22f1ad 100644 --- a/MiRCARTToolLine.py +++ b/MiRCARTToolLine.py @@ -26,6 +26,7 @@ from MiRCARTTool import MiRCARTTool class MiRCARTToolLine(MiRCARTTool): """XXX""" + name = "Line" toolOriginPoint = toolState = None TS_NONE = 0 diff --git a/MiRCARTToolRect.py b/MiRCARTToolRect.py index a1455a3..d4250ba 100644 --- a/MiRCARTToolRect.py +++ b/MiRCARTToolRect.py @@ -26,6 +26,7 @@ from MiRCARTTool import MiRCARTTool class MiRCARTToolRect(MiRCARTTool): """XXX""" + name = "Rectangle" # # onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown, dispatchFn, eventDc): XXX diff --git a/MiRCARTToolText.py b/MiRCARTToolText.py index ebb58fb..a3eb686 100644 --- a/MiRCARTToolText.py +++ b/MiRCARTToolText.py @@ -27,6 +27,7 @@ import wx class MiRCARTToolText(MiRCARTTool): """XXX""" + name = "Text" textColours = textPos = None # From 2254a0638bd19f661cfe659010c4cda93201261b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Wed, 10 Jan 2018 14:51:25 +0100 Subject: [PATCH 103/148] MiRCART{Canvas{,Interface},Frame}.py: use dict() to cache canvas state in Frame. --- MiRCARTCanvas.py | 12 +++---- MiRCARTCanvasInterface.py | 19 ++++++----- MiRCARTFrame.py | 67 ++++++++++++++++----------------------- 3 files changed, 42 insertions(+), 56 deletions(-) diff --git a/MiRCARTCanvas.py b/MiRCARTCanvas.py index 7c4c67e..eef5348 100644 --- a/MiRCARTCanvas.py +++ b/MiRCARTCanvas.py @@ -48,7 +48,7 @@ class MiRCARTCanvas(wx.Panel): for patch in deltaPatches: if self.canvasBackend.drawPatch(eventDc, patch): self._commitPatch(patch) - self.parentFrame.onCanvasUpdate(newUndoLevel=self.canvasJournal.patchesUndoLevel) + self.parentFrame.onCanvasUpdate(undoLevel=self.canvasJournal.patchesUndoLevel) # }}} # {{{ _dispatchPatch(self, eventDc, isCursor, patch): XXX def _dispatchPatch(self, eventDc, isCursor, patch): @@ -98,10 +98,10 @@ class MiRCARTCanvas(wx.Panel): event.Dragging(), event.LeftIsDown(), event.RightIsDown(), \ self._dispatchPatch, eventDc) if self._canvasDirty: - self.parentFrame.onCanvasUpdate(newCellPos=self.brushPos, \ - newUndoLevel=self.canvasJournal.patchesUndoLevel) + self.parentFrame.onCanvasUpdate(cellPos=self.brushPos, \ + undoLevel=self.canvasJournal.patchesUndoLevel) if eventType == wx.wxEVT_MOTION: - self.parentFrame.onCanvasUpdate(newCellPos=mapPoint) + self.parentFrame.onCanvasUpdate(cellPos=mapPoint) # }}} # {{{ onPanelLeaveWindow(self, event): XXX def onPanelLeaveWindow(self, event): @@ -151,7 +151,7 @@ class MiRCARTCanvas(wx.Panel): self.canvasBackend.reset(self.canvasSize, self.canvasBackend.cellSize) self.canvasJournal.resetCursor(); self.canvasJournal.resetUndo(); self.parentFrame.onCanvasUpdate( \ - newSize=self.canvasSize, newUndoLevel=-1) + size=self.canvasSize, undoLevel=-1) # }}} # @@ -164,7 +164,7 @@ class MiRCARTCanvas(wx.Panel): self.canvasMap = None; self.canvasPos = canvasPos; self.canvasSize = canvasSize; self.brushColours = [4, 1]; self.brushPos = [0, 0]; self.brushSize = [1, 1]; self.parentFrame.onCanvasUpdate( \ - newBrushSize=self.brushSize, newColours=self.brushColours) + brushSize=self.brushSize, colours=self.brushColours) self.canvasBackend = MiRCARTCanvasBackend(canvasSize, cellSize) self.canvasJournal = MiRCARTCanvasJournal() self.canvasExportStore = MiRCARTCanvasExportStore(parentCanvas=self) diff --git a/MiRCARTCanvasInterface.py b/MiRCARTCanvasInterface.py index 2222452..ee5188b 100644 --- a/MiRCARTCanvasInterface.py +++ b/MiRCARTCanvasInterface.py @@ -69,7 +69,7 @@ class MiRCARTCanvasInterface(): self.parentCanvas.brushColours[0] = numColour elif event.GetEventType() == wx.wxEVT_TOOL_RCLICKED: self.parentCanvas.brushColours[1] = numColour - self.parentFrame.onCanvasUpdate(newColours=self.parentCanvas.brushColours) + self.parentFrame.onCanvasUpdate(colours=self.parentCanvas.brushColours) # }}} # {{{ canvasCopy(self, event): XXX def canvasCopy(self, event): @@ -85,7 +85,7 @@ class MiRCARTCanvasInterface(): and self.parentCanvas.brushSize[1] > 1: self.parentCanvas.brushSize = \ [a-1 for a in self.parentCanvas.brushSize] - self.parentFrame.onCanvasUpdate(newBrushSize=self.parentCanvas.brushSize) + self.parentFrame.onCanvasUpdate(brushSize=self.parentCanvas.brushSize) # }}} # {{{ canvasDecrCanvasHeight(self, event): XXX def canvasDecrCanvasHeight(self, event): @@ -166,7 +166,7 @@ class MiRCARTCanvasInterface(): def canvasIncrBrush(self, event): self.parentCanvas.brushSize = \ [a+1 for a in self.parentCanvas.brushSize] - self.parentFrame.onCanvasUpdate(newBrushSize=self.parentCanvas.brushSize) + self.parentFrame.onCanvasUpdate(brushSize=self.parentCanvas.brushSize) # }}} # {{{ canvasIncrCanvasHeight(self, event): XXX def canvasIncrCanvasHeight(self, event): @@ -196,8 +196,7 @@ class MiRCARTCanvasInterface(): self.parentCanvas.canvasImportStore.importNew(newCanvasSize) self.canvasPathName = None self.parentCanvas.SetCursor(wx.Cursor(wx.NullCursor)) - self.parentFrame.onCanvasUpdate( \ - newPathName="", newUndoLevel=-1) + self.parentFrame.onCanvasUpdate(pathName="", undoLevel=-1) # }}} # {{{ canvasOpen(self, event): XXX def canvasOpen(self, event): @@ -220,7 +219,7 @@ class MiRCARTCanvasInterface(): self.parentCanvas.canvasImportStore.importIntoPanel() self.parentCanvas.SetCursor(wx.Cursor(wx.NullCursor)) self.parentFrame.onCanvasUpdate( \ - newPathName=self.canvasPathName, newUndoLevel=-1) + pathName=self.canvasPathName, undoLevel=-1) return True # }}} # {{{ canvasPaste(self, event): XXX @@ -261,22 +260,22 @@ class MiRCARTCanvasInterface(): # {{{ canvasToolCircle(self, event): XXX def canvasToolCircle(self, event): self.canvasTool = MiRCARTToolCircle(self.parentCanvas) - self.parentFrame.onCanvasUpdate(newToolName=self.canvasTool.name) + self.parentFrame.onCanvasUpdate(toolName=self.canvasTool.name) # }}} # {{{ canvasToolLine(self, event): XXX def canvasToolLine(self, event): self.canvasTool = MiRCARTToolLine(self.parentCanvas) - self.parentFrame.onCanvasUpdate(newToolName=self.canvasTool.name) + self.parentFrame.onCanvasUpdate(toolName=self.canvasTool.name) # }}} # {{{ canvasToolRect(self, event): XXX def canvasToolRect(self, event): self.canvasTool = MiRCARTToolRect(self.parentCanvas) - self.parentFrame.onCanvasUpdate(newToolName=self.canvasTool.name) + self.parentFrame.onCanvasUpdate(toolName=self.canvasTool.name) # }}} # {{{ canvasToolText(self, event): XXX def canvasToolText(self, event): self.canvasTool = MiRCARTToolText(self.parentCanvas) - self.parentFrame.onCanvasUpdate(newToolName=self.canvasTool.name) + self.parentFrame.onCanvasUpdate(toolName=self.canvasTool.name) # }}} # {{{ canvasUndo(self, event): XXX def canvasUndo(self, event): diff --git a/MiRCARTFrame.py b/MiRCARTFrame.py index 8a92dbf..5ad1732 100644 --- a/MiRCARTFrame.py +++ b/MiRCARTFrame.py @@ -33,8 +33,7 @@ import os, wx class MiRCARTFrame(MiRCARTGeneralFrame): """XXX""" - panelCanvas = None - lastBrushSize = lastCellPos = lastColours = lastPathName = lastSize = lastToolName = lastUndoLevel = None + panelCanvas = None; lastPanelState = {}; # {{{ Commands # Id Type Id Labels Icon bitmap Accelerator [Initial state] @@ -159,55 +158,43 @@ class MiRCARTFrame(MiRCARTGeneralFrame): self.itemsById[eventId][7](self.panelCanvas.canvasInterface, event) # }}} # {{{ onCanvasUpdate(self, newBrushSize=None, newCellPos=None, newColours=None, newPathName=None, newSize=None, newToolName=None, newUndoLevel=None): XXX - def onCanvasUpdate(self, newBrushSize=None, newCellPos=None, newColours=None, newPathName=None, newSize=None, newToolName=None, newUndoLevel=None): - if newBrushSize != None: - self.lastBrushSize = newBrushSize - if newCellPos != None: - self.lastCellPos = newCellPos - if newColours != None: - self.lastColours = newColours - if newPathName != None: - self.lastPathName = newPathName - if newSize != None: - self.lastSize = newSize - if newToolName != None: - self.lastToolName = newToolName - if newUndoLevel != None: - self.lastUndoLevel = newUndoLevel + def onCanvasUpdate(self, **kwargs): + self.lastPanelState.update(kwargs) textItems = [] - if self.lastCellPos != None: - textItems.append("X: {:03d} Y: {:03d}".format( \ - *self.lastCellPos)) - if self.lastSize != None: - textItems.append("W: {:03d} H: {:03d}".format( \ - *self.lastSize)) - if self.lastBrushSize != None: - textItems.append("Brush: {:02d}x{:02d}".format( \ - *self.lastBrushSize)) - if self.lastColours != None: - textItems.append("FG: {:02d}, BG: {:02d}".format( \ - *self.lastColours)) - textItems.append("{} on {}".format( \ - MiRCARTColours[self.lastColours[0]][4], \ - MiRCARTColours[self.lastColours[1]][4])) - if self.lastPathName != None: - if self.lastPathName != "": - basePathName = os.path.basename(self.lastPathName) + if "cellPos" in self.lastPanelState: + textItems.append("X: {:03d} Y: {:03d}".format( \ + *self.lastPanelState["cellPos"])) + if "size" in self.lastPanelState: + textItems.append("W: {:03d} H: {:03d}".format( \ + *self.lastPanelState["size"])) + if "brushSize" in self.lastPanelState: + textItems.append("Brush: {:02d}x{:02d}".format( \ + *self.lastPanelState["brushSize"])) + if "colours" in self.lastPanelState: + textItems.append("FG: {:02d}, BG: {:02d}".format( \ + *self.lastPanelState["colours"])) + textItems.append("{} on {}".format( \ + MiRCARTColours[self.lastPanelState["colours"][0]][4], \ + MiRCARTColours[self.lastPanelState["colours"][1]][4])) + if "pathName" in self.lastPanelState: + if self.lastPanelState["pathName"] != "": + basePathName = os.path.basename(self.lastPanelState["pathName"]) textItems.append("Current file: {}".format(basePathName)) self.SetTitle("{} - MiRCART".format(basePathName)) else: self.SetTitle("MiRCART") - if self.lastToolName != None: - textItems.append("Current tool: {}".format(self.lastToolName)) + if "toolName" in self.lastPanelState: + textItems.append("Current tool: {}".format( \ + self.lastPanelState["toolName"])) self.statusBar.SetStatusText(" | ".join(textItems)) - if self.lastUndoLevel != None: - if self.lastUndoLevel >= 0: + if "undoLevel" in self.lastPanelState: + if self.lastPanelState["undoLevel"] >= 0: self.menuItemsById[self.CID_UNDO[0]].Enable(True) self.toolBar.EnableTool(self.CID_UNDO[0], True) else: self.menuItemsById[self.CID_UNDO[0]].Enable(False) self.toolBar.EnableTool(self.CID_UNDO[0], False) - if self.lastUndoLevel > 0: + if self.lastPanelState["undoLevel"] > 0: self.menuItemsById[self.CID_REDO[0]].Enable(True) self.toolBar.EnableTool(self.CID_REDO[0], True) else: From 49ef54ef768400c59ab78a1c83354cca3b024cb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Wed, 10 Jan 2018 14:53:35 +0100 Subject: [PATCH 104/148] MiRCARTToolLine.py: correctly cache colours on first click. --- MiRCARTToolLine.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/MiRCARTToolLine.py b/MiRCARTToolLine.py index d22f1ad..fe14df7 100644 --- a/MiRCARTToolLine.py +++ b/MiRCARTToolLine.py @@ -27,7 +27,7 @@ from MiRCARTTool import MiRCARTTool class MiRCARTToolLine(MiRCARTTool): """XXX""" name = "Line" - toolOriginPoint = toolState = None + toolColours = toolOriginPoint = toolState = None TS_NONE = 0 TS_ORIGIN = 1 @@ -80,21 +80,25 @@ class MiRCARTToolLine(MiRCARTTool): brushColours[1] = brushColours[0] if self.toolState == self.TS_NONE: if isLeftDown or isRightDown: + self.toolColours = brushColours self.toolOriginPoint = list(atPoint) self.toolState = self.TS_ORIGIN dispatchFn(eventDc, True, [atPoint, brushColours, 0, " "]) elif self.toolState == self.TS_ORIGIN: targetPoint = list(atPoint) originPoint = self.toolOriginPoint - self._getLine(brushColours, brushSize, \ - eventDc, isLeftDown or isRightDown, \ + self._getLine(self.toolColours, brushSize, \ + eventDc, isLeftDown or isRightDown, \ originPoint, targetPoint, dispatchFn) if isLeftDown or isRightDown: + self.toolColours = None + self.toolOriginPoint = None self.toolState = self.TS_NONE # __init__(self, *args): initialisation method def __init__(self, *args): super().__init__(*args) + self.toolColours = None self.toolOriginPoint = None self.toolState = self.TS_NONE From c9d6b90e689a4a558f153c8066a19e7598832d73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Wed, 10 Jan 2018 15:04:59 +0100 Subject: [PATCH 105/148] MiRCART{anvas{,ImportStore,Interface},Frame}.py: pass & initialise from default canvas position, size, and cell size. --- MiRCARTCanvas.py | 15 +++++++++------ MiRCARTCanvasImportStore.py | 6 +++--- MiRCARTCanvasInterface.py | 2 +- MiRCARTFrame.py | 8 +++++--- 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/MiRCARTCanvas.py b/MiRCARTCanvas.py index eef5348..03b3b93 100644 --- a/MiRCARTCanvas.py +++ b/MiRCARTCanvas.py @@ -32,6 +32,7 @@ import wx class MiRCARTCanvas(wx.Panel): """XXX""" parentFrame = None + defaultCanvasPos = defaultCanvasSize = defaultCellSize = None canvasMap = canvasPos = canvasSize = None brushColours = brushPos = brushSize = None canvasBackend = canvasJournal = None @@ -155,17 +156,19 @@ class MiRCARTCanvas(wx.Panel): # }}} # - # _init__(self, parent, parentFrame, canvasPos, canvasSize, cellSize): initialisation method - def __init__(self, parent, parentFrame, canvasPos, canvasSize, cellSize): - super().__init__(parent, pos=canvasPos, \ - size=[w*h for w,h in zip(canvasSize, cellSize)]) + # _init__(self, parent, parentFrame, defaultCanvasPos, defaultCanvasSize, defaultCellSize): initialisation method + def __init__(self, parent, parentFrame, defaultCanvasPos, defaultCanvasSize, defaultCellSize): + super().__init__(parent, pos=defaultCanvasPos, \ + size=[w*h for w,h in zip(defaultCanvasSize, defaultCellSize)]) self.parentFrame = parentFrame - self.canvasMap = None; self.canvasPos = canvasPos; self.canvasSize = canvasSize; + self.canvasMap = None + self.canvasPos = defaultCanvasPos; self.canvasSize = defaultCanvasSize; + self.defaultCanvasPos = defaultCanvasPos; self.defaultCanvasSize = defaultCanvasSize; self.brushColours = [4, 1]; self.brushPos = [0, 0]; self.brushSize = [1, 1]; self.parentFrame.onCanvasUpdate( \ brushSize=self.brushSize, colours=self.brushColours) - self.canvasBackend = MiRCARTCanvasBackend(canvasSize, cellSize) + self.canvasBackend = MiRCARTCanvasBackend(defaultCanvasSize, defaultCellSize) self.canvasJournal = MiRCARTCanvasJournal() self.canvasExportStore = MiRCARTCanvasExportStore(parentCanvas=self) self.canvasImportStore = MiRCARTCanvasImportStore(parentCanvas=self) diff --git a/MiRCARTCanvasImportStore.py b/MiRCARTCanvasImportStore.py index 6bce3bf..9dc2c54 100644 --- a/MiRCARTCanvasImportStore.py +++ b/MiRCARTCanvasImportStore.py @@ -137,9 +137,9 @@ class MiRCARTCanvasImportStore(): # }}} # {{{ importNew(self, newCanvasSize=None): XXX def importNew(self, newCanvasSize=None): - newMap = [[[(1, 1), 0, " "] \ - for x in range(self.parentCanvas.canvasSize[0])] \ - for y in range(self.parentCanvas.canvasSize[1])] + newMap = [[[[1, 1], 0, " "] \ + for x in range(newCanvasSize[0])] \ + for y in range(newCanvasSize[1])] self.parentCanvas.onStoreUpdate(newCanvasSize, newMap) # }}} diff --git a/MiRCARTCanvasInterface.py b/MiRCARTCanvasInterface.py index ee5188b..a0ce190 100644 --- a/MiRCARTCanvasInterface.py +++ b/MiRCARTCanvasInterface.py @@ -192,7 +192,7 @@ class MiRCARTCanvasInterface(): self.canvasSave() self.parentCanvas.SetCursor(wx.Cursor(wx.CURSOR_WAIT)) if newCanvasSize == None: - newCanvasSize = (100, 30) + newCanvasSize = list(self.parentCanvas.defaultCanvasSize) self.parentCanvas.canvasImportStore.importNew(newCanvasSize) self.canvasPathName = None self.parentCanvas.SetCursor(wx.Cursor(wx.NullCursor)) diff --git a/MiRCARTFrame.py b/MiRCARTFrame.py index 5ad1732..eab5234 100644 --- a/MiRCARTFrame.py +++ b/MiRCARTFrame.py @@ -203,12 +203,14 @@ class MiRCARTFrame(MiRCARTGeneralFrame): # }}} # - # __init__(self, parent, appSize=(840, 630), canvasPos=(25, 50), canvasSize=(125, 35), cellSize=(7, 14)): initialisation method - def __init__(self, parent, appSize=(840, 630), canvasPos=(25, 50), canvasSize=(125, 35), cellSize=(7, 14)): + # __init__(self, parent, appSize=(840, 630), defaultCanvasPos=(25, 50), defaultCanvasSize=(100, 30), defaultCellSize=(7, 14)): initialisation method + def __init__(self, parent, appSize=(840, 630), defaultCanvasPos=(25, 50), defaultCanvasSize=(100, 30), defaultCellSize=(7, 14)): self._initPaletteToolBitmaps() panelSkin = super().__init__(parent, wx.ID_ANY, "MiRCART", size=appSize) self.panelCanvas = MiRCARTCanvas(panelSkin, parentFrame=self, \ - canvasPos=canvasPos, canvasSize=canvasSize, cellSize=cellSize) + defaultCanvasPos=defaultCanvasPos, \ + defaultCanvasSize=defaultCanvasSize, \ + defaultCellSize=defaultCellSize) self.panelCanvas.canvasInterface.canvasNew(None) # vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 From ad761d658e8aa5e4994dfa43622f9df076931da7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Wed, 10 Jan 2018 15:56:17 +0100 Subject: [PATCH 106/148] MiRCART{CanvasInterface,Frame}.py: adds clone & move (selection) tools. MiRCARTTool{Clone,Move}Select.py: initial implementation. assets/tool{Clone,Move}.png: added. MiRCART.png: updated. --- MiRCART.png | Bin 43602 -> 40512 bytes MiRCARTCanvasInterface.py | 12 ++++ MiRCARTFrame.py | 47 +++++++------- MiRCARTToolSelect.py | 129 ++++++++++++++++++++++++++++++++++++++ MiRCARTToolSelectClone.py | 60 ++++++++++++++++++ MiRCARTToolSelectMove.py | 61 ++++++++++++++++++ assets/toolClone.png | Bin 0 -> 287 bytes assets/toolMove.png | Bin 0 -> 303 bytes 8 files changed, 287 insertions(+), 22 deletions(-) create mode 100644 MiRCARTToolSelect.py create mode 100644 MiRCARTToolSelectClone.py create mode 100644 MiRCARTToolSelectMove.py create mode 100644 assets/toolClone.png create mode 100644 assets/toolMove.png diff --git a/MiRCART.png b/MiRCART.png index 8db9e9391678888bc477891c71e9622f2e83cb58..9150d4f92fc2b274ae376043593878c0cf8fe69d 100644 GIT binary patch literal 40512 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 literal 43602 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

F@dVhnL0yN8Bo0x&I=| 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 +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +from MiRCARTTool import MiRCARTTool + +class MiRCARTToolSelect(MiRCARTTool): + """XXX""" + toolColours = toolRect = toolState = None + toolLastAtPoint = None + toolSelectMap = None + srcRect = None + + TS_NONE = 0 + TS_ORIGIN = 1 + TS_TARGET = 2 + + # {{{ _drawSelectRect(self, rect, dispatchFn, eventDc): XXX + def _drawSelectRect(self, rect, dispatchFn, eventDc): + rectFrame = [ \ + [rect[0][0]-1, rect[0][1]-1], \ + [rect[1][0]+1, rect[1][1]+1]] + if rectFrame[0][0] > rectFrame[1][0]: + rectFrame[0][0], rectFrame[1][0] = \ + rectFrame[1][0], rectFrame[0][0] + if rectFrame[0][1] > rectFrame[1][1]: + rectFrame[0][1], rectFrame[1][1] = \ + rectFrame[1][1], rectFrame[0][1] + curColours = [0, 0] + for rectX in range(rectFrame[0][0], rectFrame[1][0]+1): + if curColours == [0, 0]: + curColours = [1, 1] + else: + curColours = [0, 0] + dispatchFn(eventDc, True, \ + [[rectX, rectFrame[0][1]], curColours, 0, " "]) + dispatchFn(eventDc, True, \ + [[rectX, rectFrame[1][1]], curColours, 0, " "]) + for rectY in range(rectFrame[0][1], rectFrame[1][1]+1): + if curColours == [0, 0]: + curColours = [1, 1] + else: + curColours = [0, 0] + dispatchFn(eventDc, True, \ + [[rectFrame[0][0], rectY], curColours, 0, " "]) + dispatchFn(eventDc, True, \ + [[rectFrame[1][0], rectY], curColours, 0, " "]) + # }}} + + # + # onSelectEvent(self, event, atPoint, selectRect, brushColours, brushSize, isLeftDown, isRightDown, dispatchFn, eventDc): XXX + def onSelectEvent(self, event, atPoint, selectRect, brushColours, brushSize, isLeftDown, isRightDown, dispatchFn, eventDc): + pass + + # + # onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown, dispatchFn, eventDc): XXX + def onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown, dispatchFn, eventDc): + if self.toolState == self.TS_NONE: + if isLeftDown or isRightDown: + self.toolColours = [0, 1] + self.toolRect = [list(atPoint), []] + self.toolState = self.TS_ORIGIN + else: + dispatchFn(eventDc, True, \ + [list(atPoint), brushColours.copy(), 0, " "]) + elif self.toolState == self.TS_ORIGIN: + self.toolRect[1] = list(atPoint) + if isLeftDown or isRightDown: + if self.toolRect[0][0] > self.toolRect[1][0]: + self.toolRect[0][0], self.toolRect[1][0] = \ + self.toolRect[1][0], self.toolRect[0][0] + if self.toolRect[0][1] > self.toolRect[1][1]: + self.toolRect[0][1], self.toolRect[1][1] = \ + self.toolRect[1][1], self.toolRect[0][1] + self.srcRect = self.toolRect[0] + self.toolLastAtPoint = list(atPoint) + self.toolState = self.TS_TARGET + self.toolSelectMap = [] + for numRow in range((self.toolRect[1][1] - self.toolRect[0][1]) + 1): + self.toolSelectMap.append([]) + for numCol in range((self.toolRect[1][0] - self.toolRect[0][0]) + 1): + rectY = self.toolRect[0][1] + numRow + rectX = self.toolRect[0][0] + numCol + self.toolSelectMap[numRow].append( \ + self.parentCanvas.canvasMap[rectY][rectX]) + self._drawSelectRect(self.toolRect, dispatchFn, eventDc) + elif self.toolState == self.TS_TARGET: + if isRightDown: + self.onSelectEvent(event, atPoint, self.toolRect, \ + brushColours, brushSize, isLeftDown, isRightDown, \ + dispatchFn, eventDc) + self.toolColours = None + self.toolRect = None + self.toolState = self.TS_NONE + else: + self.onSelectEvent(event, atPoint, self.toolRect, \ + brushColours, brushSize, isLeftDown, isRightDown, \ + dispatchFn, eventDc) + + # __init__(self, *args): initialisation method + def __init__(self, *args): + super().__init__(*args) + self.toolColours = None + self.toolRect = None + self.toolState = self.TS_NONE + self.toolLastAtPoint = None + self.toolSelectMap = None + +# vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/MiRCARTToolSelectClone.py b/MiRCARTToolSelectClone.py new file mode 100644 index 0000000..14c6024 --- /dev/null +++ b/MiRCARTToolSelectClone.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +# +# MiRCARTToolSelectClone.py -- XXX +# Copyright (c) 2018 Lucio Andrés Illanes Albornoz +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +from MiRCARTToolSelect import MiRCARTToolSelect + +class MiRCARTToolSelectClone(MiRCARTToolSelect): + """XXX""" + name = "Clone selection" + + # + # onSelectEvent(self, event, atPoint, selectRect, brushColours, brushSize, isLeftDown, isRightDown, dispatchFn, eventDc): XXX + def onSelectEvent(self, event, atPoint, selectRect, brushColours, brushSize, isLeftDown, isRightDown, dispatchFn, eventDc): + if isLeftDown: + atPoint = list(atPoint) + disp = [atPoint[0]-self.toolLastAtPoint[0], \ + atPoint[1]-self.toolLastAtPoint[1]] + self.toolLastAtPoint = atPoint + newToolRect = [ \ + [selectRect[0][0]+disp[0], selectRect[0][1]+disp[1]], \ + [selectRect[1][0]+disp[0], selectRect[1][1]+disp[1]]] + isCursor = True + elif isRightDown: + disp = [0, 0] + newToolRect = selectRect.copy() + isCursor = False + else: + disp = [0, 0] + newToolRect = selectRect.copy() + isCursor = True + for numRow in range(len(self.toolSelectMap)): + for numCol in range(len(self.toolSelectMap[numRow])): + cellOld = self.toolSelectMap[numRow][numCol] + rectY = selectRect[0][1] + numRow + rectX = selectRect[0][0] + numCol + dispatchFn(eventDc, isCursor, [[rectX+disp[0], rectY+disp[1]], *cellOld]) + self._drawSelectRect(newToolRect, dispatchFn, eventDc) + self.toolRect = newToolRect + +# vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/MiRCARTToolSelectMove.py b/MiRCARTToolSelectMove.py new file mode 100644 index 0000000..520f4de --- /dev/null +++ b/MiRCARTToolSelectMove.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# +# MiRCARTToolSelectMove.py -- XXX +# Copyright (c) 2018 Lucio Andrés Illanes Albornoz +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +from MiRCARTToolSelect import MiRCARTToolSelect + +class MiRCARTToolSelectMove(MiRCARTToolSelect): + """XXX""" + name = "Move selection" + + # + # onSelectEvent(self, event, atPoint, selectRect, brushColours, brushSize, isLeftDown, isRightDown, dispatchFn, eventDc): XXX + def onSelectEvent(self, event, atPoint, selectRect, brushColours, brushSize, isLeftDown, isRightDown, dispatchFn, eventDc): + if isLeftDown: + atPoint = list(atPoint) + disp = [atPoint[0]-self.toolLastAtPoint[0], \ + atPoint[1]-self.toolLastAtPoint[1]] + self.toolLastAtPoint = atPoint + newToolRect = [ \ + [selectRect[0][0]+disp[0], selectRect[0][1]+disp[1]], \ + [selectRect[1][0]+disp[0], selectRect[1][1]+disp[1]]] + isCursor = True + elif isRightDown: + disp = [0, 0] + newToolRect = selectRect.copy() + isCursor = False + else: + disp = [0, 0] + newToolRect = selectRect.copy() + isCursor = True + for numRow in range(len(self.toolSelectMap)): + for numCol in range(len(self.toolSelectMap[numRow])): + cellOld = self.toolSelectMap[numRow][numCol] + rectY = selectRect[0][1] + numRow + rectX = selectRect[0][0] + numCol + dispatchFn(eventDc, isCursor, [[self.srcRect[0] + numCol, self.srcRect[1] + numRow], [1, 1], 0, " "]) + dispatchFn(eventDc, isCursor, [[rectX+disp[0], rectY+disp[1]], *cellOld]) + self._drawSelectRect(newToolRect, dispatchFn, eventDc) + self.toolRect = newToolRect + +# vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/assets/toolClone.png b/assets/toolClone.png new file mode 100644 index 0000000000000000000000000000000000000000..fe6d1b5d9b26b85e9cbc688a3d77128343f6a5c2 GIT binary patch literal 287 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Ea{HEjtmSN`?>!lvI6;>1s;*b z3=Dh+L6~vJ#O${~LCF%=h?3y^w370~qEv>0#LT=By}Z;C1rt33JwwYY*EXd970vN< zaSXBWUmA3q_pk$x*VTXNjqmmc9Y4V#emgz*%?@9Aj~5LM`#1F%{!5asJDgn5kiF+-Th(VuQv{7wJyBmlh6oOymp!;gTe~DWM4fy^C_; literal 0 HcmV?d00001 diff --git a/assets/toolMove.png b/assets/toolMove.png new file mode 100644 index 0000000000000000000000000000000000000000..cd6d372b6f3c97ca1319c4694c5fa4f2091c1887 GIT binary patch literal 303 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Ea{HEjtmSN`?>!lvI6;>1s;*b z3=Dh+L6~vJ#O${~LCF%=h?3y^w370~qEv>0#LT=By}Z;C1rt33JwwYY*EXd96|M1f zaSXBWUmE1ecgTT9o2k(C(V74IkBILme8A{D$8(K}Z>X5}HH*BQ(&%Fn^&3t1KfHD0 zVAlQ?g9dYJ{X>&qHhKi>s<+J*MqsO;T^lShc0mbP;fh;B}T7*ffDnv(|#Wk z)xXbNJj3GFOULEYmu`5bp{Bu8!Ty6waMQln1sukQYS`aZyQOz^-nrs@Z8no^TI2L1 vMj8JX&eIfTpT&CBe|O=J*0nz^kAGrIjoKXYspG9T(A^B4u6{1-oD!M Date: Wed, 10 Jan 2018 19:20:35 +0100 Subject: [PATCH 107/148] MiRCARTToolSelectMove.py: clear source region prior to moving. --- MiRCARTToolSelectMove.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/MiRCARTToolSelectMove.py b/MiRCARTToolSelectMove.py index 520f4de..7412062 100644 --- a/MiRCARTToolSelectMove.py +++ b/MiRCARTToolSelectMove.py @@ -48,12 +48,15 @@ class MiRCARTToolSelectMove(MiRCARTToolSelect): disp = [0, 0] newToolRect = selectRect.copy() isCursor = True + for numRow in range(len(self.toolSelectMap)): + for numCol in range(len(self.toolSelectMap[numRow])): + dispatchFn(eventDc, isCursor, [[self.srcRect[0] + numCol, \ + self.srcRect[1] + numRow], [1, 1], 0, " "]) for numRow in range(len(self.toolSelectMap)): for numCol in range(len(self.toolSelectMap[numRow])): cellOld = self.toolSelectMap[numRow][numCol] rectY = selectRect[0][1] + numRow rectX = selectRect[0][0] + numCol - dispatchFn(eventDc, isCursor, [[self.srcRect[0] + numCol, self.srcRect[1] + numRow], [1, 1], 0, " "]) dispatchFn(eventDc, isCursor, [[rectX+disp[0], rectY+disp[1]], *cellOld]) self._drawSelectRect(newToolRect, dispatchFn, eventDc) self.toolRect = newToolRect From 33df272b39a6e91e2be8546004ecd32f317a9751 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Wed, 10 Jan 2018 21:28:15 +0100 Subject: [PATCH 108/148] MiRCARTCanvas.py:MiRCARTCanvas.resize(): call SetSize() w/o changing position. MiRCARTCanvas.py:MiRCARTCanvas.resize(): call SetMinSize() in addition to SetSize(). MiRCARTCanvas.py:MiRCARTCanvas.resize(): call Layout() on canvas panel window and its parents. MiRCARTCanvasInterface.py: always call SetCursor() w/ self.parentCanvas. MiRCARTFrame.py: moves colour toolbar items to separate toolbar. MiRCARTFrame.py:MiRCARTFrame.__init__(): add canvas panel to and initialise sizer. MiRCARTGeneralFrame.py: adds support for multiple toolbars via vertical box sizer. --- MiRCARTCanvas.py | 11 +++++++--- MiRCARTCanvasInterface.py | 22 ++++++++++---------- MiRCARTFrame.py | 42 ++++++++++++++++++++++++--------------- MiRCARTGeneralFrame.py | 37 ++++++++++++++++++++++++---------- 4 files changed, 71 insertions(+), 41 deletions(-) diff --git a/MiRCARTCanvas.py b/MiRCARTCanvas.py index 03b3b93..90df9d3 100644 --- a/MiRCARTCanvas.py +++ b/MiRCARTCanvas.py @@ -146,9 +146,14 @@ class MiRCARTCanvas(wx.Panel): for numNewCol in range(newCanvasSize[0]): self.canvasMap[numNewRow].append([[1, 1], 0, " "]) self.canvasSize = newCanvasSize - self.SetSize(*self.canvasPos, \ - *[a*b for a,b in zip(self.canvasSize, \ - self.canvasBackend.cellSize)]) + newWinSize = [a*b for a,b in \ + zip(self.canvasSize, self.canvasBackend.cellSize)] + self.SetMinSize(newWinSize) + self.SetSize(wx.DefaultCoord, wx.DefaultCoord, *newWinSize) + curWindow = self + while curWindow != None: + curWindow.Layout() + curWindow = curWindow.GetParent() self.canvasBackend.reset(self.canvasSize, self.canvasBackend.cellSize) self.canvasJournal.resetCursor(); self.canvasJournal.resetUndo(); self.parentFrame.onCanvasUpdate( \ diff --git a/MiRCARTCanvasInterface.py b/MiRCARTCanvasInterface.py index b05c368..ce29216 100644 --- a/MiRCARTCanvasInterface.py +++ b/MiRCARTCanvasInterface.py @@ -119,18 +119,18 @@ class MiRCARTCanvasInterface(): return False else: outPathName = dialog.GetPath() - self.SetCursor(wx.Cursor(wx.CURSOR_WAIT)) + self.parentCanvas.SetCursor(wx.Cursor(wx.CURSOR_WAIT)) self.parentCanvas.canvasExportStore.exportBitmapToPngFile( \ self.parentCanvas.canvasBackend.canvasBitmap, outPathName, \ wx.BITMAP_TYPE_PNG) - self.SetCursor(wx.Cursor(wx.NullCursor)) + self.parentCanvas.SetCursor(wx.Cursor(wx.NullCursor)) return True # }}} # {{{ canvasExportImgur(self, event): XXX def canvasExportImgur(self, event): self.parentCanvas.SetCursor(wx.Cursor(wx.CURSOR_WAIT)) imgurResult = self.parentCanvas.canvasExportStore.exportBitmapToImgur( \ - "c9a6efb3d7932fd", self.parentCanvas.canvasBackend.canvasBitmap, \ + "c9a6efb3d7932fd", self.parentCanvas.canvasBackend.canvasBitmap, \ "", "", wx.BITMAP_TYPE_PNG) self.parentCanvas.SetCursor(wx.Cursor(wx.NullCursor)) if imgurResult[0] == 200: @@ -138,21 +138,21 @@ class MiRCARTCanvasInterface(): wx.TheClipboard.Open() wx.TheClipboard.SetData(wx.TextDataObject(imgurResult[1])) wx.TheClipboard.Close() - wx.MessageBox("Exported to Imgur: " + imgurResult[1], \ + wx.MessageBox("Exported to Imgur: " + imgurResult[1], \ "Export to Imgur", wx.OK|wx.ICON_INFORMATION) else: - wx.MessageBox("Failed to export to Imgur: " + imgurResult[1], \ + wx.MessageBox("Failed to export to Imgur: " + imgurResult[1], \ "Export to Imgur", wx.OK|wx.ICON_EXCLAMATION) # }}} # {{{ canvasExportPastebin(self, event): XXX def canvasExportPastebin(self, event): - self.SetCursor(wx.Cursor(wx.CURSOR_WAIT)) + self.parentCanvas.SetCursor(wx.Cursor(wx.CURSOR_WAIT)) pasteStatus, pasteResult = \ - self.parentCanvas.canvasExportStore.exportPastebin( \ + self.parentCanvas.canvasExportStore.exportPastebin( \ "", \ - self.parentCanvas.canvasMap, \ + self.parentCanvas.canvasMap, \ self.parentCanvas.canvasSize) - self.SetCursor(wx.Cursor(wx.NullCursor)) + self.parentCanvas.SetCursor(wx.Cursor(wx.NullCursor)) if pasteStatus: if not wx.TheClipboard.IsOpened(): wx.TheClipboard.Open() @@ -240,11 +240,11 @@ class MiRCARTCanvasInterface(): return try: with open(self.canvasPathName, "w") as outFile: - self.SetCursor(wx.Cursor(wx.CURSOR_WAIT)) + self.parentCanvas.SetCursor(wx.Cursor(wx.CURSOR_WAIT)) self.parentCanvas.canvasExportStore.exportTextFile( \ self.parentCanvas.canvasMap, \ self.parentCanvas.canvasSize, outFile) - self.SetCursor(wx.Cursor(wx.NullCursor)) + self.parentCanvas.SetCursor(wx.Cursor(wx.NullCursor)) return True except IOError as error: return False diff --git a/MiRCARTFrame.py b/MiRCARTFrame.py index 1fe1115..d4e9735 100644 --- a/MiRCARTFrame.py +++ b/MiRCARTFrame.py @@ -27,7 +27,7 @@ from MiRCARTCanvasInterface import MiRCARTCanvasInterface from MiRCARTColours import MiRCARTColours from MiRCARTGeneralFrame import MiRCARTGeneralFrame, \ TID_ACCELS, TID_COMMAND, TID_LIST, TID_MENU, TID_NOTHING, TID_SELECT, TID_TOOLBAR, \ - NID_MENU_SEP, NID_TOOLBAR_SEP + NID_MENU_SEP, NID_TOOLBAR_HSEP, NID_TOOLBAR_VSEP import os, wx @@ -107,11 +107,12 @@ class MiRCARTFrame(MiRCARTGeneralFrame): # }}} # {{{ Toolbars BID_TOOLBAR = (0x400, TID_TOOLBAR, ( \ - CID_NEW, CID_OPEN, CID_SAVE, CID_SAVEAS, NID_TOOLBAR_SEP, \ - CID_UNDO, CID_REDO, NID_TOOLBAR_SEP, \ - CID_CUT, CID_COPY, CID_PASTE, CID_DELETE, NID_TOOLBAR_SEP, \ - CID_INCR_BRUSH, CID_DECR_BRUSH, NID_TOOLBAR_SEP, \ - CID_RECT, CID_CIRCLE, CID_LINE, CID_TEXT, CID_CLONE_SELECT, CID_MOVE_SELECT, NID_TOOLBAR_SEP, \ + CID_NEW, CID_OPEN, CID_SAVE, CID_SAVEAS, NID_TOOLBAR_HSEP, \ + CID_UNDO, CID_REDO, NID_TOOLBAR_HSEP, \ + CID_CUT, CID_COPY, CID_PASTE, CID_DELETE, NID_TOOLBAR_HSEP, \ + CID_INCR_BRUSH, CID_DECR_BRUSH, NID_TOOLBAR_HSEP, \ + CID_RECT, CID_CIRCLE, CID_LINE, CID_TEXT, CID_CLONE_SELECT, CID_MOVE_SELECT, \ + NID_TOOLBAR_VSEP, \ CID_COLOUR00, CID_COLOUR01, CID_COLOUR02, CID_COLOUR03, CID_COLOUR04, \ CID_COLOUR05, CID_COLOUR06, CID_COLOUR07, CID_COLOUR08, CID_COLOUR09, \ CID_COLOUR10, CID_COLOUR11, CID_COLOUR12, CID_COLOUR13, CID_COLOUR14, \ @@ -193,27 +194,36 @@ class MiRCARTFrame(MiRCARTGeneralFrame): if "undoLevel" in self.lastPanelState: if self.lastPanelState["undoLevel"] >= 0: self.menuItemsById[self.CID_UNDO[0]].Enable(True) - self.toolBar.EnableTool(self.CID_UNDO[0], True) + toolBar = self.toolBarItemsById[self.CID_UNDO[0]].GetToolBar() + toolBar.EnableTool(self.CID_UNDO[0], True) else: self.menuItemsById[self.CID_UNDO[0]].Enable(False) - self.toolBar.EnableTool(self.CID_UNDO[0], False) + toolBar = self.toolBarItemsById[self.CID_UNDO[0]].GetToolBar() + toolBar.EnableTool(self.CID_UNDO[0], False) if self.lastPanelState["undoLevel"] > 0: self.menuItemsById[self.CID_REDO[0]].Enable(True) - self.toolBar.EnableTool(self.CID_REDO[0], True) + toolBar = self.toolBarItemsById[self.CID_REDO[0]].GetToolBar() + toolBar.EnableTool(self.CID_REDO[0], True) else: self.menuItemsById[self.CID_REDO[0]].Enable(False) - self.toolBar.EnableTool(self.CID_REDO[0], False) + toolBar = self.toolBarItemsById[self.CID_REDO[0]].GetToolBar() + toolBar.EnableTool(self.CID_REDO[0], False) # }}} # - # __init__(self, parent, appSize=(840, 630), defaultCanvasPos=(25, 50), defaultCanvasSize=(100, 30), defaultCellSize=(7, 14)): initialisation method - def __init__(self, parent, appSize=(840, 630), defaultCanvasPos=(25, 50), defaultCanvasSize=(100, 30), defaultCellSize=(7, 14)): + # __init__(self, parent, appSize=(840, 630), defaultCanvasPos=(0, 75), defaultCanvasSize=(100, 30), defaultCellSize=(7, 14)): initialisation method + def __init__(self, parent, appSize=(840, 630), defaultCanvasPos=(0, 75), defaultCanvasSize=(100, 30), defaultCellSize=(7, 14)): self._initPaletteToolBitmaps() - panelSkin = super().__init__(parent, wx.ID_ANY, "MiRCART", size=appSize) - self.panelCanvas = MiRCARTCanvas(panelSkin, parentFrame=self, \ - defaultCanvasPos=defaultCanvasPos, \ - defaultCanvasSize=defaultCanvasSize, \ + self.panelSkin = super().__init__(parent, wx.ID_ANY, "MiRCART", size=appSize) + self.panelCanvas = MiRCARTCanvas(self.panelSkin, parentFrame=self, \ + defaultCanvasPos=defaultCanvasPos, \ + defaultCanvasSize=defaultCanvasSize, \ defaultCellSize=defaultCellSize) self.panelCanvas.canvasInterface.canvasNew(None) + self.sizerSkin.AddSpacer(5) + self.sizerSkin.Add(self.panelCanvas, wx.ALL|wx.EXPAND) + self.panelSkin.SetSizer(self.sizerSkin) + self.panelSkin.SetAutoLayout(1) + self.sizerSkin.Fit(self.panelSkin) # vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/MiRCARTGeneralFrame.py b/MiRCARTGeneralFrame.py index 75afc7c..baf3355 100644 --- a/MiRCARTGeneralFrame.py +++ b/MiRCARTGeneralFrame.py @@ -37,12 +37,14 @@ TID_TOOLBAR = (0x007) # # Non-items NID_MENU_SEP = (0x200, TID_NOTHING) -NID_TOOLBAR_SEP = (0x201, TID_NOTHING) +NID_TOOLBAR_HSEP = (0x201, TID_NOTHING) +NID_TOOLBAR_VSEP = (0x202, TID_NOTHING) class MiRCARTGeneralFrame(wx.Frame): """XXX""" itemsById = menuItemsById = toolBarItemsById = None - statusBar = toolBar = None + statusBar = toolBars = None + panelSkin = sizerSkin = None # {{{ _initAccelTable(self, accelsDescr): XXX def _initAccelTable(self, accelsDescr): @@ -83,28 +85,41 @@ class MiRCARTGeneralFrame(wx.Frame): # {{{ _initToolBars(self, toolBarsDescr, panelSkin): XXX def _initToolBars(self, toolBarsDescr, panelSkin): self.toolBarItemsById = {} - self.toolBar = wx.ToolBar(panelSkin, -1, \ - style=wx.HORIZONTAL|wx.TB_FLAT|wx.TB_NODIVIDER) - self.toolBar.SetToolBitmapSize((16,16)) + self.sizerSkin = wx.BoxSizer(wx.VERTICAL) + self.toolBars = [None]; numToolBar = 0; for toolBarItem in toolBarsDescr[2]: - if toolBarItem == NID_TOOLBAR_SEP: - self.toolBar.AddSeparator() + if self.toolBars[numToolBar] == None: + self.toolBars[numToolBar] = \ + wx.ToolBar(panelSkin, -1, \ + style=wx.HORIZONTAL|wx.TB_FLAT|wx.TB_NODIVIDER) + self.toolBars[numToolBar].SetToolBitmapSize((16,16)) + if toolBarItem == NID_TOOLBAR_HSEP: + self.toolBars[numToolBar].AddSeparator() + elif toolBarItem == NID_TOOLBAR_VSEP: + numToolBar += 1; self.toolBars.append(None); else: self.itemsById[toolBarItem[0]] = toolBarItem - toolBarItemWindow = self.toolBar.AddTool( \ - toolBarItem[0], toolBarItem[2], toolBarItem[4][2]) + toolBarItemWindow = \ + self.toolBars[numToolBar].AddTool( \ + toolBarItem[0], toolBarItem[2], \ + toolBarItem[4][2]) self.toolBarItemsById[toolBarItem[0]] = toolBarItemWindow if toolBarItem[6] != None \ and toolBarItem[1] == TID_COMMAND: toolBarItemWindow.Enable(toolBarItem[6]) self.Bind(wx.EVT_TOOL, self.onInput, toolBarItemWindow) self.Bind(wx.EVT_TOOL_RCLICKED, self.onInput, toolBarItemWindow) - self.toolBar.Realize(); self.toolBar.Fit(); + for numToolBar in range(len(self.toolBars)): + self.sizerSkin.Add( \ + self.toolBars[numToolBar], 0, wx.ALIGN_LEFT, 4) + self.toolBars[numToolBar].Realize() + self.toolBars[numToolBar].Fit() # }}} # {{{ _initToolBitmaps(self, toolBarsDescr): XXX def _initToolBitmaps(self, toolBarsDescr): for toolBarItem in toolBarsDescr[2]: - if toolBarItem == NID_TOOLBAR_SEP: + if toolBarItem == NID_TOOLBAR_HSEP \ + or toolBarItem == NID_TOOLBAR_VSEP: continue elif toolBarItem[4] == None: toolBarItem[4] = ["", None, wx.ArtProvider.GetBitmap( \ From e53e8bf8e771a03c5331732ee4ed495a2d4f55d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Wed, 10 Jan 2018 22:16:48 +0100 Subject: [PATCH 109/148] MiRCARTFrame.py: add canvas panel window to sizer w/ border width 14. --- MiRCARTFrame.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiRCARTFrame.py b/MiRCARTFrame.py index d4e9735..95665f0 100644 --- a/MiRCARTFrame.py +++ b/MiRCARTFrame.py @@ -221,7 +221,7 @@ class MiRCARTFrame(MiRCARTGeneralFrame): defaultCellSize=defaultCellSize) self.panelCanvas.canvasInterface.canvasNew(None) self.sizerSkin.AddSpacer(5) - self.sizerSkin.Add(self.panelCanvas, wx.ALL|wx.EXPAND) + self.sizerSkin.Add(self.panelCanvas, 0, wx.ALL|wx.EXPAND, 14) self.panelSkin.SetSizer(self.sizerSkin) self.panelSkin.SetAutoLayout(1) self.sizerSkin.Fit(self.panelSkin) From 499296ba1de21f1a61c7a5b2d759e52adf624bc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Wed, 10 Jan 2018 22:31:52 +0100 Subject: [PATCH 110/148] MiRCARTGeneralFrame.py: add toolbar windows to sizer w/ border width 3. --- MiRCARTGeneralFrame.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiRCARTGeneralFrame.py b/MiRCARTGeneralFrame.py index baf3355..978b89f 100644 --- a/MiRCARTGeneralFrame.py +++ b/MiRCARTGeneralFrame.py @@ -111,7 +111,7 @@ class MiRCARTGeneralFrame(wx.Frame): self.Bind(wx.EVT_TOOL_RCLICKED, self.onInput, toolBarItemWindow) for numToolBar in range(len(self.toolBars)): self.sizerSkin.Add( \ - self.toolBars[numToolBar], 0, wx.ALIGN_LEFT, 4) + self.toolBars[numToolBar], 0, wx.ALL|wx.ALIGN_LEFT, 3) self.toolBars[numToolBar].Realize() self.toolBars[numToolBar].Fit() # }}} From d26a3226129c1bbb76c7e31d4c607639c436e7de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Thu, 11 Jan 2018 00:26:46 +0100 Subject: [PATCH 111/148] MiRCART{CanvasInterface,Frame}.py: adds (flood) fill tool. MiRCARTToolFill.py: initial implementation. assets/toolFill.png: added. --- MiRCARTCanvasInterface.py | 6 +++++ MiRCARTFrame.py | 15 ++++++----- MiRCARTToolFill.py | 53 ++++++++++++++++++++++++++++++++++++++ assets/toolFill.png | Bin 0 -> 527 bytes 4 files changed, 67 insertions(+), 7 deletions(-) create mode 100644 MiRCARTToolFill.py create mode 100644 assets/toolFill.png diff --git a/MiRCARTCanvasInterface.py b/MiRCARTCanvasInterface.py index ce29216..43dcd8c 100644 --- a/MiRCARTCanvasInterface.py +++ b/MiRCARTCanvasInterface.py @@ -23,6 +23,7 @@ # from MiRCARTToolCircle import MiRCARTToolCircle +from MiRCARTToolFill import MiRCARTToolFill from MiRCARTToolLine import MiRCARTToolLine from MiRCARTToolSelectClone import MiRCARTToolSelectClone from MiRCARTToolSelectMove import MiRCARTToolSelectMove @@ -264,6 +265,11 @@ class MiRCARTCanvasInterface(): self.canvasTool = MiRCARTToolCircle(self.parentCanvas) self.parentFrame.onCanvasUpdate(toolName=self.canvasTool.name) # }}} + # {{{ canvasToolFill(self, event): XXX + def canvasToolFill(self, event): + self.canvasTool = MiRCARTToolFill(self.parentCanvas) + self.parentFrame.onCanvasUpdate(toolName=self.canvasTool.name) + # }}} # {{{ canvasToolLine(self, event): XXX def canvasToolLine(self, event): self.canvasTool = MiRCARTToolLine(self.parentCanvas) diff --git a/MiRCARTFrame.py b/MiRCARTFrame.py index 95665f0..e357e48 100644 --- a/MiRCARTFrame.py +++ b/MiRCARTFrame.py @@ -70,10 +70,11 @@ class MiRCARTFrame(MiRCARTGeneralFrame): CID_RECT = [0x150, TID_SELECT, "Rectangle", "&Rectangle", ["toolRect.png"], [wx.ACCEL_CTRL, ord("R")], True, MiRCARTCanvasInterface.canvasToolRect] CID_CIRCLE = [0x151, TID_SELECT, "Circle", "&Circle", ["toolCircle.png"], [wx.ACCEL_CTRL, ord("C")], False, MiRCARTCanvasInterface.canvasToolCircle] - CID_LINE = [0x152, TID_SELECT, "Line", "&Line", ["toolLine.png"], [wx.ACCEL_CTRL, ord("L")], False, MiRCARTCanvasInterface.canvasToolLine] - CID_TEXT = [0x153, TID_SELECT, "Text", "&Text", ["toolText.png"], [wx.ACCEL_CTRL, ord("T")], False, MiRCARTCanvasInterface.canvasToolText] - CID_CLONE_SELECT = [0x154, TID_SELECT, "Clone", "Cl&one", ["toolClone.png"], [wx.ACCEL_CTRL, ord("E")], False, MiRCARTCanvasInterface.canvasToolSelectClone] - CID_MOVE_SELECT = [0x155, TID_SELECT, "Move", "&Move", ["toolMove.png"], [wx.ACCEL_CTRL, ord("M")], False, MiRCARTCanvasInterface.canvasToolSelectMove] + CID_FILL = [0x152, TID_SELECT, "Fill", "&Fill", ["toolFill.png"], [wx.ACCEL_CTRL, ord("F")], False, MiRCARTCanvasInterface.canvasToolFill] + CID_LINE = [0x153, TID_SELECT, "Line", "&Line", ["toolLine.png"], [wx.ACCEL_CTRL, ord("L")], False, MiRCARTCanvasInterface.canvasToolLine] + CID_TEXT = [0x154, TID_SELECT, "Text", "&Text", ["toolText.png"], [wx.ACCEL_CTRL, ord("T")], False, MiRCARTCanvasInterface.canvasToolText] + CID_CLONE_SELECT = [0x155, TID_SELECT, "Clone", "Cl&one", ["toolClone.png"], [wx.ACCEL_CTRL, ord("E")], False, MiRCARTCanvasInterface.canvasToolSelectClone] + CID_MOVE_SELECT = [0x156, TID_SELECT, "Move", "&Move", ["toolMove.png"], [wx.ACCEL_CTRL, ord("M")], False, MiRCARTCanvasInterface.canvasToolSelectMove] CID_COLOUR00 = [0x1a0, TID_COMMAND, "Colour #00", "Colour #00", None, None, None, MiRCARTCanvasInterface.canvasColour] CID_COLOUR01 = [0x1a1, TID_COMMAND, "Colour #01", "Colour #01", None, None, None, MiRCARTCanvasInterface.canvasColour] @@ -103,7 +104,7 @@ class MiRCARTFrame(MiRCARTGeneralFrame): CID_INCRW_CANVAS, CID_DECRW_CANVAS, CID_INCRH_CANVAS, CID_DECRH_CANVAS, NID_MENU_SEP, \ CID_INCR_BRUSH, CID_DECR_BRUSH, CID_SOLID_BRUSH)) MID_TOOLS = (0x302, TID_MENU, "Tools", "&Tools", ( \ - CID_RECT, CID_CIRCLE, CID_LINE, CID_TEXT, CID_CLONE_SELECT, CID_MOVE_SELECT)) + CID_RECT, CID_CIRCLE, CID_FILL, CID_LINE, CID_TEXT, CID_CLONE_SELECT, CID_MOVE_SELECT)) # }}} # {{{ Toolbars BID_TOOLBAR = (0x400, TID_TOOLBAR, ( \ @@ -111,7 +112,7 @@ class MiRCARTFrame(MiRCARTGeneralFrame): CID_UNDO, CID_REDO, NID_TOOLBAR_HSEP, \ CID_CUT, CID_COPY, CID_PASTE, CID_DELETE, NID_TOOLBAR_HSEP, \ CID_INCR_BRUSH, CID_DECR_BRUSH, NID_TOOLBAR_HSEP, \ - CID_RECT, CID_CIRCLE, CID_LINE, CID_TEXT, CID_CLONE_SELECT, CID_MOVE_SELECT, \ + CID_RECT, CID_CIRCLE, CID_FILL, CID_LINE, CID_TEXT, CID_CLONE_SELECT, CID_MOVE_SELECT, \ NID_TOOLBAR_VSEP, \ CID_COLOUR00, CID_COLOUR01, CID_COLOUR02, CID_COLOUR03, CID_COLOUR04, \ CID_COLOUR05, CID_COLOUR06, CID_COLOUR07, CID_COLOUR08, CID_COLOUR09, \ @@ -123,7 +124,7 @@ class MiRCARTFrame(MiRCARTGeneralFrame): CID_NEW, CID_OPEN, CID_SAVE, CID_UNDO, CID_REDO, \ CID_INCRW_CANVAS, CID_DECRW_CANVAS, CID_INCRH_CANVAS, CID_DECRH_CANVAS, \ CID_INCR_BRUSH, CID_DECR_BRUSH, \ - CID_RECT, CID_CIRCLE, CID_LINE, CID_TEXT, CID_CLONE_SELECT, CID_MOVE_SELECT)) + CID_RECT, CID_CIRCLE, CID_FILL, CID_LINE, CID_TEXT, CID_CLONE_SELECT, CID_MOVE_SELECT)) # }}} # {{{ Lists LID_ACCELS = (0x600, TID_LIST, (AID_EDIT)) diff --git a/MiRCARTToolFill.py b/MiRCARTToolFill.py new file mode 100644 index 0000000..3a7ef22 --- /dev/null +++ b/MiRCARTToolFill.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +# +# MiRCARTToolFill.py -- XXX +# Copyright (c) 2018 Lucio Andrés Illanes Albornoz +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +from MiRCARTTool import MiRCARTTool + +class MiRCARTToolFill(MiRCARTTool): + """XXX""" + name = "Fill" + + # + # onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown, dispatchFn, eventDc): XXX + def onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown, dispatchFn, eventDc): + pointStack = [list(atPoint)] + if isLeftDown or isRightDown: + if isRightDown: + brushColours = [brushColours[1], brushColours[0]] + while len(pointStack) > 0: + point = pointStack.pop() + pointCell = self.parentCanvas.canvasMap[point[1]][point[0]] + if pointCell[0][1] == brushColours[1]: + dispatchFn(eventDc, False, [point.copy(), \ + [brushColours[0], brushColours[0]], 0, " "]) + if point[0] > 0: + pointStack.append([point[0] - 1, point[1]]) + if point[0] < (self.parentCanvas.canvasSize[0] - 1): + pointStack.append([point[0] + 1, point[1]]) + if point[1] > 0: + pointStack.append([point[0], point[1] - 1]) + if point[1] < (self.parentCanvas.canvasSize[1] - 1): + pointStack.append([point[0], point[1] + 1]) + +# vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/assets/toolFill.png b/assets/toolFill.png new file mode 100644 index 0000000000000000000000000000000000000000..54b87375fee3affffea33f859879b0b1a91bbf55 GIT binary patch literal 527 zcmV+q0`UEbP)N2bZe?^J zG%heMF*(%MvSa`N0gOpRK~y+Tg_1!_0znvt_XqS3`Uyg}ZguWbM>`aBtxH`+=(4yJ zDISatD~KYof~?`8z(n?t)Is#1C@8U|EQX=%Li7XP<{Q{$)^+XheDlpaI}ba|NC4JS zsZ`jodW~?KV=;6|r_*c*<|0%M8QWs$BFi##U1x1@&4tPyBO-=`s;Y>^VsIR1t%S2a?Go+iW49*HJ8L zTtK(rN@lZJK*Gz>r3E=zf@wM^m7bByJwTRk(CKuL$z)&{h9{X$r#^{vs=Q1WiaXnf!PX`V%@%wfYn)ks=3=AV}a! zOw;~XG9HgnC=?)V?%soN1+HW`{0^10+iiBVEZ-wtt8Hv--SLTtrfKN+zXFna{RQF^ z=S9AK^8yiijlG`bNp#)dS1o?y+pr2nQMjbp9I&gES>#{$O~RlDMJ(q4q(AFc0+ZY6 ReRBW+002ovPDHLkV1jBW;cEZ@ literal 0 HcmV?d00001 From f65603dd0c53fe03d380200f71f33914b0f7969f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Thu, 11 Jan 2018 00:31:25 +0100 Subject: [PATCH 112/148] MiRCART.png: updated. --- MiRCART.png | Bin 40512 -> 52285 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/MiRCART.png b/MiRCART.png index 9150d4f92fc2b274ae376043593878c0cf8fe69d..fff39a2214a4ea8c2cd33e83fa5254b7bdf2fa76 100644 GIT binary patch literal 52285 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�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

sSTcqDoC{d?F6LcJZg~89rH2X~px4u;RZw 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 literal 40512 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 From 4bc9b61f2c68527aed8e1ab2a94fb6f80609ae33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Thu, 11 Jan 2018 00:45:40 +0100 Subject: [PATCH 113/148] MiRCARTGeneralFrame.py: show accelerators (hotkeys) in menus. --- MiRCARTGeneralFrame.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/MiRCARTGeneralFrame.py b/MiRCARTGeneralFrame.py index 978b89f..d294b12 100644 --- a/MiRCARTGeneralFrame.py +++ b/MiRCARTGeneralFrame.py @@ -42,18 +42,20 @@ NID_TOOLBAR_VSEP = (0x202, TID_NOTHING) class MiRCARTGeneralFrame(wx.Frame): """XXX""" - itemsById = menuItemsById = toolBarItemsById = None + accelItemsById = itemsById = menuItemsById = toolBarItemsById = None statusBar = toolBars = None panelSkin = sizerSkin = None # {{{ _initAccelTable(self, accelsDescr): XXX def _initAccelTable(self, accelsDescr): accelTableEntries = [wx.AcceleratorEntry() for n in range(len(accelsDescr[2]))] + self.accelItemsById = {} for numAccel in range(len(accelsDescr[2])): accelDescr = accelsDescr[2][numAccel] if accelDescr[5] != None: self.itemsById[accelDescr[0]] = accelDescr accelTableEntries[numAccel].Set(*accelDescr[5], accelDescr[0]) + self.accelItemsById[accelDescr[0]] = accelTableEntries[numAccel] self.Bind(wx.EVT_MENU, self.onInput, id=accelDescr[0]) return accelTableEntries # }}} @@ -68,6 +70,8 @@ class MiRCARTGeneralFrame(wx.Frame): elif menuItem[1] == TID_SELECT: self.itemsById[menuItem[0]] = menuItem menuItemWindow = menuWindow.AppendRadioItem(menuItem[0], menuItem[3], menuItem[2]) + if menuItem[5] != None: + menuItemWindow.SetAccel(self.accelItemsById[menuItem[0]]) self.menuItemsById[menuItem[0]] = menuItemWindow self.Bind(wx.EVT_MENU, self.onInput, menuItemWindow) if menuItem[6] != None: @@ -75,6 +79,8 @@ class MiRCARTGeneralFrame(wx.Frame): else: self.itemsById[menuItem[0]] = menuItem menuItemWindow = menuWindow.Append(menuItem[0], menuItem[3], menuItem[2]) + if menuItem[5] != None: + menuItemWindow.SetAccel(self.accelItemsById[menuItem[0]]) self.menuItemsById[menuItem[0]] = menuItemWindow self.Bind(wx.EVT_MENU, self.onInput, menuItemWindow) if menuItem[6] != None: @@ -150,6 +156,11 @@ class MiRCARTGeneralFrame(wx.Frame): super().__init__(*args, **kwargs); self.itemsById = {}; panelSkin = wx.Panel(self, wx.ID_ANY) + # Initialise accelerators (hotkeys) + accelTable = wx.AcceleratorTable( \ + self._initAccelTable(self.LID_ACCELS[2])) + self.SetAcceleratorTable(accelTable) + # Initialise menu bar, menus & menu items # Initialise toolbar & toolbar items menuBar = self._initMenus(self.LID_MENUS[2]) @@ -157,11 +168,6 @@ class MiRCARTGeneralFrame(wx.Frame): self._initToolBitmaps(self.LID_TOOLBARS[2]) toolBar = self._initToolBars(self.LID_TOOLBARS[2], panelSkin) - # Initialise accelerators (hotkeys) - accelTable = wx.AcceleratorTable( \ - self._initAccelTable(self.LID_ACCELS[2])) - self.SetAcceleratorTable(accelTable) - # Initialise status bar self.statusBar = self.CreateStatusBar() From ebdf7f410b29b20430ccb8d982a77744620d09ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Thu, 11 Jan 2018 00:59:07 +0100 Subject: [PATCH 114/148] MiRCARTCanvasInterface.py: sync menu item state when selecting tool. --- MiRCARTCanvasInterface.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/MiRCARTCanvasInterface.py b/MiRCARTCanvasInterface.py index 43dcd8c..f9d373e 100644 --- a/MiRCARTCanvasInterface.py +++ b/MiRCARTCanvasInterface.py @@ -263,36 +263,43 @@ class MiRCARTCanvasInterface(): # {{{ canvasToolCircle(self, event): XXX def canvasToolCircle(self, event): self.canvasTool = MiRCARTToolCircle(self.parentCanvas) + self.parentFrame.menuItemsById[self.parentFrame.CID_CIRCLE[0]].Check(True) self.parentFrame.onCanvasUpdate(toolName=self.canvasTool.name) # }}} # {{{ canvasToolFill(self, event): XXX def canvasToolFill(self, event): self.canvasTool = MiRCARTToolFill(self.parentCanvas) + self.parentFrame.menuItemsById[self.parentFrame.CID_FILL[0]].Check(True) self.parentFrame.onCanvasUpdate(toolName=self.canvasTool.name) # }}} # {{{ canvasToolLine(self, event): XXX def canvasToolLine(self, event): self.canvasTool = MiRCARTToolLine(self.parentCanvas) + self.parentFrame.menuItemsById[self.parentFrame.CID_LINE[0]].Check(True) self.parentFrame.onCanvasUpdate(toolName=self.canvasTool.name) # }}} # {{{ canvasToolSelectClone(self, event): XXX def canvasToolSelectClone(self, event): self.canvasTool = MiRCARTToolSelectClone(self.parentCanvas) + self.parentFrame.menuItemsById[self.parentFrame.CID_CLONE_SELECT[0]].Check(True) self.parentFrame.onCanvasUpdate(toolName=self.canvasTool.name) # }}} # {{{ canvasToolSelectMove(self, event): XXX def canvasToolSelectMove(self, event): self.canvasTool = MiRCARTToolSelectMove(self.parentCanvas) + self.parentFrame.menuItemsById[self.parentFrame.CID_MOVE_SELECT[0]].Check(True) self.parentFrame.onCanvasUpdate(toolName=self.canvasTool.name) # }}} # {{{ canvasToolRect(self, event): XXX def canvasToolRect(self, event): self.canvasTool = MiRCARTToolRect(self.parentCanvas) + self.parentFrame.menuItemsById[self.parentFrame.CID_RECT[0]].Check(True) self.parentFrame.onCanvasUpdate(toolName=self.canvasTool.name) # }}} # {{{ canvasToolText(self, event): XXX def canvasToolText(self, event): self.canvasTool = MiRCARTToolText(self.parentCanvas) + self.parentFrame.menuItemsById[self.parentFrame.CID_TEXT[0]].Check(True) self.parentFrame.onCanvasUpdate(toolName=self.canvasTool.name) # }}} # {{{ canvasUndo(self, event): XXX From 02d3c7f6f49a749ed869c70013bb8670699c335c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Thu, 11 Jan 2018 01:09:00 +0100 Subject: [PATCH 115/148] assets/tool{Clone,Move}.png: updated. --- assets/toolClone.png | Bin 287 -> 227 bytes assets/toolMove.png | Bin 303 -> 213 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/assets/toolClone.png b/assets/toolClone.png index fe6d1b5d9b26b85e9cbc688a3d77128343f6a5c2..ea358d3606cf23f08883ff55b51e618922a4b8ce 100644 GIT binary patch delta 138 zcmV;50CoSL0^lepsav0xJ#*69ZGfN6_)LrYOsm=`a}&YmOu1!W#<7f98qb6BSP$^ZZW07*qoM6N<$g4Yu}&j0`b delta 199 zcmV;&0672S0iObpJPN@801m+cxRGn^kwz$goJmAMR5(wiQriu~KnzON{%3*izf#A@ z2p8LDQ|!1+A4uqcfWNYgG5%s3e#2vMf_E*b(CH2sC{rQU z%LHWQgBi+Hh|L}K!KcWuVMFw>gP6Gb%OhqQ+YVv^!hs<{>aPzN$fpC5$HqLw1jHLM zNWD2=W*&$md`xxxP+QutL0C}p)MfJv+wgOaWe1#Z!Ax{&&^rJC002ovPDHLkV1mI0 BUuggU diff --git a/assets/toolMove.png b/assets/toolMove.png index cd6d372b6f3c97ca1319c4694c5fa4f2091c1887..c52a274a42fe54c305d694acea7d12938b2ad228 100644 GIT binary patch delta 124 zcmV-?0E7Rp0@VSKJPN=701m(bYSxJfkwz$IQb|NXR5(wyjzJ0lAP5Bg|EG;4O=>|c zJPa-)E)p|;0cC|9B<81Lp@J`gvI3M5{U6LSgx_0+avhxL-gAizz6{C=P)789pe5NU eS;H$(=3yGb9l2xh{s&|L0000}0j~m(JPN@801m+cxRGn^kwz$gtVu*cR5(wiQb`WNKnyDaaYf?H|G&f+ zxN+bCL!3#hBu!NoOV&7UY;{}47=N)fzv0^0!D_#RFn~8ZFT#`QgYXW$S5UnQ?sNr; z;GKn6h`P5`phNI*+hBxTE{~uh1IEiv@L?zSnWLCE+UY^3m!+`gC?+TjcnA0j5wgBQ zU7!dv!h8qrdq!u9iQLpf)|&zyW`UQ)GHCyyo+%Rtng!KQyK(r1t@$~}@&;vEvsChk R?Mnav002ovPDHLkV1gK1V)Fn1 From cf04a12690ad68ca339ce37231accc1a58cda5c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Thu, 11 Jan 2018 01:12:55 +0100 Subject: [PATCH 116/148] MiRCARTCanvasInterface.py: prompt to save changes on exit given non-None canvasPathName. --- MiRCARTCanvasInterface.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/MiRCARTCanvasInterface.py b/MiRCARTCanvasInterface.py index f9d373e..3451401 100644 --- a/MiRCARTCanvasInterface.py +++ b/MiRCARTCanvasInterface.py @@ -110,6 +110,14 @@ class MiRCARTCanvasInterface(): # }}} # {{{ canvasExit(self, event): XXX def canvasExit(self, event): + if self.canvasPathName != None: + saveChanges = self._dialogSaveChanges() + if saveChanges == wx.ID_CANCEL: + return + elif saveChanges == wx.ID_NO: + pass + elif saveChanges == wx.ID_YES: + self.canvasSave() self.parentFrame.Close(True) # }}} # {{{ canvasExportAsPng(self, event): XXX From d1936486ff659b30c97590608dfe69e7e8d86774 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Thu, 11 Jan 2018 01:18:50 +0100 Subject: [PATCH 117/148] MiRCART.py: import argv[1] into canvas if specified. --- MiRCART.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/MiRCART.py b/MiRCART.py index c03e793..d7bd121 100755 --- a/MiRCART.py +++ b/MiRCART.py @@ -29,7 +29,13 @@ import sys, wx # Entry point def main(*argv): wxApp = wx.App(False) - MiRCARTFrame(None) + appFrame = MiRCARTFrame(None) + if len(argv) > 1 \ + and len(argv[1]) > 0: + appFrame.panelCanvas.canvasInterface.canvasPathName = argv[1] + appFrame.panelCanvas.canvasImportStore.importTextFile(argv[1]) + appFrame.panelCanvas.canvasImportStore.importIntoPanel() + appFrame.onCanvasUpdate(pathName=argv[1], undoLevel=-1) wxApp.MainLoop() if __name__ == "__main__": main(*sys.argv) From bb243c75565498ff0b9686764728c3c3a6f29e1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Thu, 11 Jan 2018 01:52:53 +0100 Subject: [PATCH 118/148] MiRCARTCanvasInterface.py:_updateCanvasSize(): reimplement (fixes [WASD] bugs.) --- MiRCARTCanvasInterface.py | 40 ++++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/MiRCARTCanvasInterface.py b/MiRCARTCanvasInterface.py index 3451401..cc0bb80 100644 --- a/MiRCARTCanvasInterface.py +++ b/MiRCARTCanvasInterface.py @@ -48,17 +48,39 @@ class MiRCARTCanvasInterface(): # {{{ _updateCanvasSize(self, newCanvasSize): XXX def _updateCanvasSize(self, newCanvasSize): eventDc = self.parentCanvas.canvasBackend.getDeviceContext(self.parentCanvas) + oldCanvasSize = self.parentCanvas.canvasSize self.parentCanvas.resize(newCanvasSize) - self.parentCanvas.canvasBackend.resize( \ - newCanvasSize, \ + self.parentCanvas.canvasBackend.resize( \ + newCanvasSize, \ self.parentCanvas.canvasBackend.cellSize) - for numRow in range(self.parentCanvas.canvasSize[1] - 1): - self.parentCanvas.canvasMap.append([[1, 1], 0, " "]) - self.parentCanvas.canvasMap.append([]) - for numCol in range(self.parentCanvas.canvasSize[0]): - self.parentCanvas.canvasMap[-1].append([[1, 1], 0, " "]) - self.parentCanvas.canvasBackend.drawPatch(eventDc, \ - ([numCol, self.parentCanvas.canvasSize[1] - 1], *[[1, 1], 0, " "])) + if (newCanvasSize[1] - oldCanvasSize[1]) < 0: + for numRowOff in range(1, (oldCanvasSize[1] - newCanvasSize[1]) + 1): + numRow = oldCanvasSize[1] - numRowOff + del self.parentCanvas.canvasMap[numRow] + else: + for numRowOff in range(oldCanvasSize[1] - newCanvasSize[1]): + numRow = oldCanvasSize[1] + numRowOff + self.parentCanvas.canvasMap.append(None) + self.parentCanvas.canvasMap[numRow] = \ + [[[1, 1], 0, " "]] * oldCanvasSize[0] + self.parentCanvas.canvasBackend.drawPatch( \ + eventDc, \ + [[numCol, numRow], *[[1, 1], 0, " "]]) + if (newCanvasSize[0] - oldCanvasSize[0]) < 0: + for numRow in range(newCanvasSize[1]): + for numColOff in range(1, (oldCanvasSize[0] - newCanvasSize[0]) + 1): + numCol = oldCanvasSize[0] - numColOff + del self.parentCanvas.canvasMap[numRow][numCol] + else: + for numRow in range(newCanvasSize[1]): + for numColOff in range(newCanvasSize[0] - oldCanvasSize[0]): + numCol = oldCanvasSize[0] + numColOff + self.parentCanvas.canvasMap[numRow].append(None) + self.parentCanvas.canvasMap[numRow][numCol] = \ + [[1, 1], 0, " "] + self.parentCanvas.canvasBackend.drawPatch( \ + eventDc, \ + [[numCol, numRow], *[[1, 1], 0, " "]]) wx.SafeYield() # }}} From 2f85fa8cd1f7aa9e24e2c836e0a38ef595b3bde1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Thu, 11 Jan 2018 01:56:28 +0100 Subject: [PATCH 119/148] MiRCARTToolFill.py: set qualifying colour from current cell. --- MiRCARTToolFill.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MiRCARTToolFill.py b/MiRCARTToolFill.py index 3a7ef22..9235275 100644 --- a/MiRCARTToolFill.py +++ b/MiRCARTToolFill.py @@ -32,13 +32,14 @@ class MiRCARTToolFill(MiRCARTTool): # onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown, dispatchFn, eventDc): XXX def onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown, dispatchFn, eventDc): pointStack = [list(atPoint)] + testColour = self.parentCanvas.canvasMap[atPoint[1]][atPoint[0]][0][1] if isLeftDown or isRightDown: if isRightDown: brushColours = [brushColours[1], brushColours[0]] while len(pointStack) > 0: point = pointStack.pop() pointCell = self.parentCanvas.canvasMap[point[1]][point[0]] - if pointCell[0][1] == brushColours[1]: + if pointCell[0][1] == testColour: dispatchFn(eventDc, False, [point.copy(), \ [brushColours[0], brushColours[0]], 0, " "]) if point[0] > 0: From 44490912b2d8cd5cd1cce77569326bc5bbab45ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Thu, 11 Jan 2018 01:59:43 +0100 Subject: [PATCH 120/148] MiRCARTCanvasInterface.py:_updateCanvasSize(): hide cursor before resizing. --- MiRCARTCanvasInterface.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MiRCARTCanvasInterface.py b/MiRCARTCanvasInterface.py index cc0bb80..0078da9 100644 --- a/MiRCARTCanvasInterface.py +++ b/MiRCARTCanvasInterface.py @@ -48,6 +48,8 @@ class MiRCARTCanvasInterface(): # {{{ _updateCanvasSize(self, newCanvasSize): XXX def _updateCanvasSize(self, newCanvasSize): eventDc = self.parentCanvas.canvasBackend.getDeviceContext(self.parentCanvas) + self.parentCanvas.canvasBackend.drawCursorMaskWithJournal( \ + self.parentCanvas.canvasJournal, eventDc) oldCanvasSize = self.parentCanvas.canvasSize self.parentCanvas.resize(newCanvasSize) self.parentCanvas.canvasBackend.resize( \ From 508a3cd48905eeed5bd4780c722f97ebd992d42d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Thu, 11 Jan 2018 02:06:09 +0100 Subject: [PATCH 121/148] MiRCARTToolFill.py: don't process cells more than once. --- MiRCARTToolFill.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/MiRCARTToolFill.py b/MiRCARTToolFill.py index 9235275..c1adc8c 100644 --- a/MiRCARTToolFill.py +++ b/MiRCARTToolFill.py @@ -31,7 +31,7 @@ class MiRCARTToolFill(MiRCARTTool): # # onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown, dispatchFn, eventDc): XXX def onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown, dispatchFn, eventDc): - pointStack = [list(atPoint)] + pointStack = [list(atPoint)]; pointsDone = []; testColour = self.parentCanvas.canvasMap[atPoint[1]][atPoint[0]][0][1] if isLeftDown or isRightDown: if isRightDown: @@ -40,15 +40,17 @@ class MiRCARTToolFill(MiRCARTTool): point = pointStack.pop() pointCell = self.parentCanvas.canvasMap[point[1]][point[0]] if pointCell[0][1] == testColour: - dispatchFn(eventDc, False, [point.copy(), \ - [brushColours[0], brushColours[0]], 0, " "]) - if point[0] > 0: - pointStack.append([point[0] - 1, point[1]]) - if point[0] < (self.parentCanvas.canvasSize[0] - 1): - pointStack.append([point[0] + 1, point[1]]) - if point[1] > 0: - pointStack.append([point[0], point[1] - 1]) - if point[1] < (self.parentCanvas.canvasSize[1] - 1): - pointStack.append([point[0], point[1] + 1]) + if not point in pointsDone: + dispatchFn(eventDc, False, [point.copy(), \ + [brushColours[0], brushColours[0]], 0, " "]) + if point[0] > 0: + pointStack.append([point[0] - 1, point[1]]) + if point[0] < (self.parentCanvas.canvasSize[0] - 1): + pointStack.append([point[0] + 1, point[1]]) + if point[1] > 0: + pointStack.append([point[0], point[1] - 1]) + if point[1] < (self.parentCanvas.canvasSize[1] - 1): + pointStack.append([point[0], point[1] + 1]) + pointsDone += [point] # vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 From 46258cde3263c208dc40f76329e7b9b93f1261cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Thu, 11 Jan 2018 02:10:33 +0100 Subject: [PATCH 122/148] MiRCARTCanvas.py:MiRCARTCanvas.onPanelInput(): clip mapPoint to canvasSize. --- MiRCARTCanvas.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/MiRCARTCanvas.py b/MiRCARTCanvas.py index 90df9d3..0d5912c 100644 --- a/MiRCARTCanvas.py +++ b/MiRCARTCanvas.py @@ -93,6 +93,9 @@ class MiRCARTCanvas(wx.Panel): event.Skip(); return; else: mapPoint = self.canvasBackend.xlateEventPoint(event, eventDc) + if mapPoint[0] >= self.canvasSize[0] \ + or mapPoint[1] >= self.canvasSize[1]: + return self.brushPos = mapPoint tool.onMouseEvent( \ event, mapPoint, self.brushColours, self.brushSize, \ From a33ff6b5dfafaa9a3eaa3782574337f85a1c4f38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Thu, 11 Jan 2018 02:34:32 +0100 Subject: [PATCH 123/148] MiRCART{CanvasInterface,{,General}Frame}.py: add & sync {tools,colour selection} toolbar items as radio tools. --- MiRCARTCanvasInterface.py | 14 ++++++++++++++ MiRCARTFrame.py | 32 ++++++++++++++++---------------- MiRCARTGeneralFrame.py | 18 +++++++++++++----- 3 files changed, 43 insertions(+), 21 deletions(-) diff --git a/MiRCARTCanvasInterface.py b/MiRCARTCanvasInterface.py index 0078da9..011d45f 100644 --- a/MiRCARTCanvasInterface.py +++ b/MiRCARTCanvasInterface.py @@ -296,42 +296,56 @@ class MiRCARTCanvasInterface(): def canvasToolCircle(self, event): self.canvasTool = MiRCARTToolCircle(self.parentCanvas) self.parentFrame.menuItemsById[self.parentFrame.CID_CIRCLE[0]].Check(True) + toolBar = self.parentFrame.toolBarItemsById[self.parentFrame.CID_CIRCLE[0]].GetToolBar() + toolBar.ToggleTool(self.parentFrame.CID_CIRCLE[0], True) self.parentFrame.onCanvasUpdate(toolName=self.canvasTool.name) # }}} # {{{ canvasToolFill(self, event): XXX def canvasToolFill(self, event): self.canvasTool = MiRCARTToolFill(self.parentCanvas) self.parentFrame.menuItemsById[self.parentFrame.CID_FILL[0]].Check(True) + toolBar = self.parentFrame.toolBarItemsById[self.parentFrame.CID_FILL[0]].GetToolBar() + toolBar.ToggleTool(self.parentFrame.CID_FILL[0], True) self.parentFrame.onCanvasUpdate(toolName=self.canvasTool.name) # }}} # {{{ canvasToolLine(self, event): XXX def canvasToolLine(self, event): self.canvasTool = MiRCARTToolLine(self.parentCanvas) self.parentFrame.menuItemsById[self.parentFrame.CID_LINE[0]].Check(True) + toolBar = self.parentFrame.toolBarItemsById[self.parentFrame.CID_LINE[0]].GetToolBar() + toolBar.ToggleTool(self.parentFrame.CID_LINE[0], True) self.parentFrame.onCanvasUpdate(toolName=self.canvasTool.name) # }}} # {{{ canvasToolSelectClone(self, event): XXX def canvasToolSelectClone(self, event): self.canvasTool = MiRCARTToolSelectClone(self.parentCanvas) self.parentFrame.menuItemsById[self.parentFrame.CID_CLONE_SELECT[0]].Check(True) + toolBar = self.parentFrame.toolBarItemsById[self.parentFrame.CID_CLONE_SELECT[0]].GetToolBar() + toolBar.ToggleTool(self.parentFrame.CID_CLONE_SELECT[0], True) self.parentFrame.onCanvasUpdate(toolName=self.canvasTool.name) # }}} # {{{ canvasToolSelectMove(self, event): XXX def canvasToolSelectMove(self, event): self.canvasTool = MiRCARTToolSelectMove(self.parentCanvas) self.parentFrame.menuItemsById[self.parentFrame.CID_MOVE_SELECT[0]].Check(True) + toolBar = self.parentFrame.toolBarItemsById[self.parentFrame.CID_MOVE_SELECT[0]].GetToolBar() + toolBar.ToggleTool(self.parentFrame.CID_MOVE_SELECT[0], True) self.parentFrame.onCanvasUpdate(toolName=self.canvasTool.name) # }}} # {{{ canvasToolRect(self, event): XXX def canvasToolRect(self, event): self.canvasTool = MiRCARTToolRect(self.parentCanvas) self.parentFrame.menuItemsById[self.parentFrame.CID_RECT[0]].Check(True) + toolBar = self.parentFrame.toolBarItemsById[self.parentFrame.CID_RECT[0]].GetToolBar() + toolBar.ToggleTool(self.parentFrame.CID_RECT[0], True) self.parentFrame.onCanvasUpdate(toolName=self.canvasTool.name) # }}} # {{{ canvasToolText(self, event): XXX def canvasToolText(self, event): self.canvasTool = MiRCARTToolText(self.parentCanvas) self.parentFrame.menuItemsById[self.parentFrame.CID_TEXT[0]].Check(True) + toolBar = self.parentFrame.toolBarItemsById[self.parentFrame.CID_TEXT[0]].GetToolBar() + toolBar.ToggleTool(self.parentFrame.CID_TEXT[0], True) self.parentFrame.onCanvasUpdate(toolName=self.canvasTool.name) # }}} # {{{ canvasUndo(self, event): XXX diff --git a/MiRCARTFrame.py b/MiRCARTFrame.py index e357e48..6c199aa 100644 --- a/MiRCARTFrame.py +++ b/MiRCARTFrame.py @@ -76,22 +76,22 @@ class MiRCARTFrame(MiRCARTGeneralFrame): CID_CLONE_SELECT = [0x155, TID_SELECT, "Clone", "Cl&one", ["toolClone.png"], [wx.ACCEL_CTRL, ord("E")], False, MiRCARTCanvasInterface.canvasToolSelectClone] CID_MOVE_SELECT = [0x156, TID_SELECT, "Move", "&Move", ["toolMove.png"], [wx.ACCEL_CTRL, ord("M")], False, MiRCARTCanvasInterface.canvasToolSelectMove] - CID_COLOUR00 = [0x1a0, TID_COMMAND, "Colour #00", "Colour #00", None, None, None, MiRCARTCanvasInterface.canvasColour] - CID_COLOUR01 = [0x1a1, TID_COMMAND, "Colour #01", "Colour #01", None, None, None, MiRCARTCanvasInterface.canvasColour] - CID_COLOUR02 = [0x1a2, TID_COMMAND, "Colour #02", "Colour #02", None, None, None, MiRCARTCanvasInterface.canvasColour] - CID_COLOUR03 = [0x1a3, TID_COMMAND, "Colour #03", "Colour #03", None, None, None, MiRCARTCanvasInterface.canvasColour] - CID_COLOUR04 = [0x1a4, TID_COMMAND, "Colour #04", "Colour #04", None, None, None, MiRCARTCanvasInterface.canvasColour] - CID_COLOUR05 = [0x1a5, TID_COMMAND, "Colour #05", "Colour #05", None, None, None, MiRCARTCanvasInterface.canvasColour] - CID_COLOUR06 = [0x1a6, TID_COMMAND, "Colour #06", "Colour #06", None, None, None, MiRCARTCanvasInterface.canvasColour] - CID_COLOUR07 = [0x1a7, TID_COMMAND, "Colour #07", "Colour #07", None, None, None, MiRCARTCanvasInterface.canvasColour] - CID_COLOUR08 = [0x1a8, TID_COMMAND, "Colour #08", "Colour #08", None, None, None, MiRCARTCanvasInterface.canvasColour] - CID_COLOUR09 = [0x1a9, TID_COMMAND, "Colour #09", "Colour #09", None, None, None, MiRCARTCanvasInterface.canvasColour] - CID_COLOUR10 = [0x1aa, TID_COMMAND, "Colour #10", "Colour #10", None, None, None, MiRCARTCanvasInterface.canvasColour] - CID_COLOUR11 = [0x1ab, TID_COMMAND, "Colour #11", "Colour #11", None, None, None, MiRCARTCanvasInterface.canvasColour] - CID_COLOUR12 = [0x1ac, TID_COMMAND, "Colour #12", "Colour #12", None, None, None, MiRCARTCanvasInterface.canvasColour] - CID_COLOUR13 = [0x1ad, TID_COMMAND, "Colour #13", "Colour #13", None, None, None, MiRCARTCanvasInterface.canvasColour] - CID_COLOUR14 = [0x1ae, TID_COMMAND, "Colour #14", "Colour #14", None, None, None, MiRCARTCanvasInterface.canvasColour] - CID_COLOUR15 = [0x1af, TID_COMMAND, "Colour #15", "Colour #15", None, None, None, MiRCARTCanvasInterface.canvasColour] + CID_COLOUR00 = [0x1a0, TID_SELECT, "Colour #00", "Colour #00", None, None, True, MiRCARTCanvasInterface.canvasColour] + CID_COLOUR01 = [0x1a1, TID_SELECT, "Colour #01", "Colour #01", None, None, False, MiRCARTCanvasInterface.canvasColour] + CID_COLOUR02 = [0x1a2, TID_SELECT, "Colour #02", "Colour #02", None, None, False, MiRCARTCanvasInterface.canvasColour] + CID_COLOUR03 = [0x1a3, TID_SELECT, "Colour #03", "Colour #03", None, None, False, MiRCARTCanvasInterface.canvasColour] + CID_COLOUR04 = [0x1a4, TID_SELECT, "Colour #04", "Colour #04", None, None, False, MiRCARTCanvasInterface.canvasColour] + CID_COLOUR05 = [0x1a5, TID_SELECT, "Colour #05", "Colour #05", None, None, False, MiRCARTCanvasInterface.canvasColour] + CID_COLOUR06 = [0x1a6, TID_SELECT, "Colour #06", "Colour #06", None, None, False, MiRCARTCanvasInterface.canvasColour] + CID_COLOUR07 = [0x1a7, TID_SELECT, "Colour #07", "Colour #07", None, None, False, MiRCARTCanvasInterface.canvasColour] + CID_COLOUR08 = [0x1a8, TID_SELECT, "Colour #08", "Colour #08", None, None, False, MiRCARTCanvasInterface.canvasColour] + CID_COLOUR09 = [0x1a9, TID_SELECT, "Colour #09", "Colour #09", None, None, False, MiRCARTCanvasInterface.canvasColour] + CID_COLOUR10 = [0x1aa, TID_SELECT, "Colour #10", "Colour #10", None, None, False, MiRCARTCanvasInterface.canvasColour] + CID_COLOUR11 = [0x1ab, TID_SELECT, "Colour #11", "Colour #11", None, None, False, MiRCARTCanvasInterface.canvasColour] + CID_COLOUR12 = [0x1ac, TID_SELECT, "Colour #12", "Colour #12", None, None, False, MiRCARTCanvasInterface.canvasColour] + CID_COLOUR13 = [0x1ad, TID_SELECT, "Colour #13", "Colour #13", None, None, False, MiRCARTCanvasInterface.canvasColour] + CID_COLOUR14 = [0x1ae, TID_SELECT, "Colour #14", "Colour #14", None, None, False, MiRCARTCanvasInterface.canvasColour] + CID_COLOUR15 = [0x1af, TID_SELECT, "Colour #15", "Colour #15", None, None, False, MiRCARTCanvasInterface.canvasColour] # }}} # {{{ Menus MID_FILE = (0x300, TID_MENU, "File", "&File", ( \ diff --git a/MiRCARTGeneralFrame.py b/MiRCARTGeneralFrame.py index d294b12..ea53385 100644 --- a/MiRCARTGeneralFrame.py +++ b/MiRCARTGeneralFrame.py @@ -103,20 +103,28 @@ class MiRCARTGeneralFrame(wx.Frame): self.toolBars[numToolBar].AddSeparator() elif toolBarItem == NID_TOOLBAR_VSEP: numToolBar += 1; self.toolBars.append(None); + elif toolBarItem[1] == TID_SELECT: + self.itemsById[toolBarItem[0]] = toolBarItem + toolBarItemWindow = \ + self.toolBars[numToolBar].AddRadioTool( \ + toolBarItem[0], toolBarItem[2], toolBarItem[4][2]) + self.toolBarItemsById[toolBarItem[0]] = toolBarItemWindow + if toolBarItem[6] != None: + toolBarItemWindow.Toggle(toolBarItem[6]) + self.Bind(wx.EVT_TOOL, self.onInput, toolBarItemWindow) + self.Bind(wx.EVT_TOOL_RCLICKED, self.onInput, toolBarItemWindow) else: self.itemsById[toolBarItem[0]] = toolBarItem toolBarItemWindow = \ self.toolBars[numToolBar].AddTool( \ - toolBarItem[0], toolBarItem[2], \ - toolBarItem[4][2]) + toolBarItem[0], toolBarItem[2], toolBarItem[4][2]) self.toolBarItemsById[toolBarItem[0]] = toolBarItemWindow - if toolBarItem[6] != None \ - and toolBarItem[1] == TID_COMMAND: + if toolBarItem[6] != None: toolBarItemWindow.Enable(toolBarItem[6]) self.Bind(wx.EVT_TOOL, self.onInput, toolBarItemWindow) self.Bind(wx.EVT_TOOL_RCLICKED, self.onInput, toolBarItemWindow) for numToolBar in range(len(self.toolBars)): - self.sizerSkin.Add( \ + self.sizerSkin.Add( \ self.toolBars[numToolBar], 0, wx.ALL|wx.ALIGN_LEFT, 3) self.toolBars[numToolBar].Realize() self.toolBars[numToolBar].Fit() From 4e19f91eb0872413975c6d8c58297eb56b0d4c82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Thu, 11 Jan 2018 02:37:20 +0100 Subject: [PATCH 124/148] MiRCARTFrame.py: adds exit accelerator (hotkey) X. --- MiRCARTFrame.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MiRCARTFrame.py b/MiRCARTFrame.py index 6c199aa..05dbeb8 100644 --- a/MiRCARTFrame.py +++ b/MiRCARTFrame.py @@ -47,7 +47,7 @@ class MiRCARTFrame(MiRCARTGeneralFrame): "Export to I&mgur...", None, None, haveUrllib, MiRCARTCanvasInterface.canvasExportImgur] CID_EXPORT_PASTEBIN = [0x106, TID_COMMAND, "Export to Pastebin...", \ "Export to Pasteb&in...", None, None, haveUrllib, MiRCARTCanvasInterface.canvasExportPastebin] - CID_EXIT = [0x107, TID_COMMAND, "Exit", "E&xit", None, None, None, MiRCARTCanvasInterface.canvasExit] + CID_EXIT = [0x107, TID_COMMAND, "Exit", "E&xit", None, [wx.ACCEL_CTRL, ord("X")], None, MiRCARTCanvasInterface.canvasExit] CID_UNDO = [0x108, TID_COMMAND, "Undo", "&Undo", ["", wx.ART_UNDO], [wx.ACCEL_CTRL, ord("Z")], False, MiRCARTCanvasInterface.canvasUndo] CID_REDO = [0x109, TID_COMMAND, "Redo", "&Redo", ["", wx.ART_REDO], [wx.ACCEL_CTRL, ord("Y")], False, MiRCARTCanvasInterface.canvasRedo] CID_CUT = [0x10a, TID_COMMAND, "Cut", "Cu&t", ["", wx.ART_CUT], None, False, MiRCARTCanvasInterface.canvasCut] @@ -121,7 +121,7 @@ class MiRCARTFrame(MiRCARTGeneralFrame): # }}} # {{{ Accelerators (hotkeys) AID_EDIT = (0x500, TID_ACCELS, ( \ - CID_NEW, CID_OPEN, CID_SAVE, CID_UNDO, CID_REDO, \ + CID_NEW, CID_OPEN, CID_SAVE, CID_EXIT, CID_UNDO, CID_REDO, \ CID_INCRW_CANVAS, CID_DECRW_CANVAS, CID_INCRH_CANVAS, CID_DECRH_CANVAS, \ CID_INCR_BRUSH, CID_DECR_BRUSH, \ CID_RECT, CID_CIRCLE, CID_FILL, CID_LINE, CID_TEXT, CID_CLONE_SELECT, CID_MOVE_SELECT)) From 5380e899e427034c3e8dedd5da7d5ceec1e8b754 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Thu, 11 Jan 2018 02:49:53 +0100 Subject: [PATCH 125/148] MiRCARTFrame.py: add {in,de}crease {height,width} toolbar items. assets/tool{De,In}crCanvas{H,W}.png: added. --- MiRCARTFrame.py | 9 +++++---- assets/toolDecrCanvasH.png | Bin 0 -> 271 bytes assets/toolDecrCanvasW.png | Bin 0 -> 250 bytes assets/toolIncrCanvasH.png | Bin 0 -> 236 bytes assets/toolIncrCanvasW.png | Bin 0 -> 232 bytes 5 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 assets/toolDecrCanvasH.png create mode 100644 assets/toolDecrCanvasW.png create mode 100644 assets/toolIncrCanvasH.png create mode 100644 assets/toolIncrCanvasW.png diff --git a/MiRCARTFrame.py b/MiRCARTFrame.py index 05dbeb8..7def5e7 100644 --- a/MiRCARTFrame.py +++ b/MiRCARTFrame.py @@ -55,13 +55,13 @@ class MiRCARTFrame(MiRCARTGeneralFrame): CID_PASTE = [0x10c, TID_COMMAND, "Paste", "&Paste", ["", wx.ART_PASTE], None, False, MiRCARTCanvasInterface.canvasPaste] CID_DELETE = [0x10d, TID_COMMAND, "Delete", "De&lete", ["", wx.ART_DELETE], None, False, MiRCARTCanvasInterface.canvasDelete] CID_INCRW_CANVAS = [0x10e, TID_COMMAND, "Increase canvas width", \ - "Increase canvas &width", ["", wx.ART_PLUS], [wx.ACCEL_ALT, ord("D")], None, MiRCARTCanvasInterface.canvasIncrCanvasWidth] + "Increase canvas &width", ["toolIncrCanvasW.png"], [wx.ACCEL_ALT, ord("D")], None, MiRCARTCanvasInterface.canvasIncrCanvasWidth] CID_DECRW_CANVAS = [0x10f, TID_COMMAND, "Decrease canvas width", \ - "Decrease canvas w&idth", ["", wx.ART_MINUS], [wx.ACCEL_ALT, ord("A")], None, MiRCARTCanvasInterface.canvasDecrCanvasWidth] + "Decrease canvas w&idth", ["toolDecrCanvasW.png"], [wx.ACCEL_ALT, ord("A")], None, MiRCARTCanvasInterface.canvasDecrCanvasWidth] CID_INCRH_CANVAS = [0x110, TID_COMMAND, "Increase canvas height", \ - "Increase canvas &height", ["", wx.ART_PLUS], [wx.ACCEL_ALT, ord("S")], None, MiRCARTCanvasInterface.canvasIncrCanvasHeight] + "Increase canvas &height", ["toolIncrCanvasH.png"], [wx.ACCEL_ALT, ord("S")], None, MiRCARTCanvasInterface.canvasIncrCanvasHeight] CID_DECRH_CANVAS = [0x111, TID_COMMAND, "Decrease canvas height", \ - "Decrease canvas h&eight", ["", wx.ART_MINUS], [wx.ACCEL_ALT, ord("W")], None, MiRCARTCanvasInterface.canvasDecrCanvasHeight] + "Decrease canvas h&eight", ["toolDecrCanvasH.png"], [wx.ACCEL_ALT, ord("W")], None, MiRCARTCanvasInterface.canvasDecrCanvasHeight] CID_INCR_BRUSH = [0x112, TID_COMMAND, "Increase brush size", \ "I&ncrease brush size", ["", wx.ART_PLUS], [wx.ACCEL_CTRL, ord("+")], None, MiRCARTCanvasInterface.canvasIncrBrush] CID_DECR_BRUSH = [0x113, TID_COMMAND, "Decrease brush size", \ @@ -112,6 +112,7 @@ class MiRCARTFrame(MiRCARTGeneralFrame): CID_UNDO, CID_REDO, NID_TOOLBAR_HSEP, \ CID_CUT, CID_COPY, CID_PASTE, CID_DELETE, NID_TOOLBAR_HSEP, \ CID_INCR_BRUSH, CID_DECR_BRUSH, NID_TOOLBAR_HSEP, \ + CID_INCRW_CANVAS, CID_DECRW_CANVAS, CID_INCRH_CANVAS, CID_DECRH_CANVAS, NID_TOOLBAR_HSEP, \ CID_RECT, CID_CIRCLE, CID_FILL, CID_LINE, CID_TEXT, CID_CLONE_SELECT, CID_MOVE_SELECT, \ NID_TOOLBAR_VSEP, \ CID_COLOUR00, CID_COLOUR01, CID_COLOUR02, CID_COLOUR03, CID_COLOUR04, \ diff --git a/assets/toolDecrCanvasH.png b/assets/toolDecrCanvasH.png new file mode 100644 index 0000000000000000000000000000000000000000..6b372738c70ceac3d8eb7dfb8d14e1aecd9592be GIT binary patch literal 271 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DkxK$!8B)5ZfpLCF%=h?3y^w370~qEv>0#LT=By}Z;C1rt33JwwYY*EXd974>+! zIEGmGFYR&UJ8ZzgGTE#>PV4Fa`KlXC-$_jfUOmmNEiyrP;(m>%T#*jFdB<*KyxuVX zqwcMsKkrW{>1_KR!l!NFJkO=}!O|NO-&N0Jk=?NCQ1O`pW=H0I0v~0J?>HQ3Ja0X9 zyXYK6|3!8iwE2Om?{#mJ_#gJT@_bM4Hoo7IkvclkOJf#J*{^(?HHWADxK5Y(A)vb$ NJYD@<);T3K0RX`gV}t+z literal 0 HcmV?d00001 diff --git a/assets/toolDecrCanvasW.png b/assets/toolDecrCanvasW.png new file mode 100644 index 0000000000000000000000000000000000000000..64e964b41c9004f4b29f1a2f3d913cd380de3c9a GIT binary patch literal 250 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DkxK$!8B)5ZfpLCF%=h?3y^w370~qEv>0#LT=By}Z;C1rt33JwwYY*EXd96_tCs zIEGmGFYO8BYcSw&?zKO*;=8@xI&PoFT^6d9o|PrDU7m=C^f^4`h-+@SY6GAX7(8A5T-G@yGywpyLR4%3 literal 0 HcmV?d00001 diff --git a/assets/toolIncrCanvasH.png b/assets/toolIncrCanvasH.png new file mode 100644 index 0000000000000000000000000000000000000000..157c81aaea85d69d2dbd72735fe95810ac6ed72a GIT binary patch literal 236 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DkxK$!8B)5ZfpLCF%=h?3y^w370~qEv>0#LT=By}Z;C1rt33JwwYY*EXd96=iz5 zIEGmGukDHEJD|Y9viJYel@sz#t&Z3tFMMZWs=!6Tm2U)EHC}7w%n;0o|7i1W(tPbj z9$jzl@wt6`&M0F0W4f%Y%L-Nz8!%%B>y9(Vfp6pvG|krrYY=o@d}hfeXRZCtkC`es W*5-#79$yQzl)=;0&t;ucLK6UCCs9ZM literal 0 HcmV?d00001 diff --git a/assets/toolIncrCanvasW.png b/assets/toolIncrCanvasW.png new file mode 100644 index 0000000000000000000000000000000000000000..6790bfc4babf737cd7a644743ccb6e1109550703 GIT binary patch literal 232 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DkxL735kHCP2GC|TkfQ4*Y=R#Ki=l*&+$n3-3imzP?iV4`QBXJ~ol+NKnsqEt^8 z#}EturM--N2NXD%|NdV-b4J3F5{nrpC3iCM3I1fAD!O8g!&1ZkxX&kxTfG+VJ-{aT zCb%g=h>aIWS)63!)#|<0l+%#9Z^xfy3H{t!Ev&08n#?z(+i&xjw&9GZ?212qJ?vA? Wtj!nQwL= Date: Thu, 11 Jan 2018 03:02:18 +0100 Subject: [PATCH 126/148] MiRCARTFrame.py: initially select red (4) colour toolbar item. --- MiRCARTFrame.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MiRCARTFrame.py b/MiRCARTFrame.py index 7def5e7..9ac8738 100644 --- a/MiRCARTFrame.py +++ b/MiRCARTFrame.py @@ -76,11 +76,11 @@ class MiRCARTFrame(MiRCARTGeneralFrame): CID_CLONE_SELECT = [0x155, TID_SELECT, "Clone", "Cl&one", ["toolClone.png"], [wx.ACCEL_CTRL, ord("E")], False, MiRCARTCanvasInterface.canvasToolSelectClone] CID_MOVE_SELECT = [0x156, TID_SELECT, "Move", "&Move", ["toolMove.png"], [wx.ACCEL_CTRL, ord("M")], False, MiRCARTCanvasInterface.canvasToolSelectMove] - CID_COLOUR00 = [0x1a0, TID_SELECT, "Colour #00", "Colour #00", None, None, True, MiRCARTCanvasInterface.canvasColour] + CID_COLOUR00 = [0x1a0, TID_SELECT, "Colour #00", "Colour #00", None, None, False, MiRCARTCanvasInterface.canvasColour] CID_COLOUR01 = [0x1a1, TID_SELECT, "Colour #01", "Colour #01", None, None, False, MiRCARTCanvasInterface.canvasColour] CID_COLOUR02 = [0x1a2, TID_SELECT, "Colour #02", "Colour #02", None, None, False, MiRCARTCanvasInterface.canvasColour] CID_COLOUR03 = [0x1a3, TID_SELECT, "Colour #03", "Colour #03", None, None, False, MiRCARTCanvasInterface.canvasColour] - CID_COLOUR04 = [0x1a4, TID_SELECT, "Colour #04", "Colour #04", None, None, False, MiRCARTCanvasInterface.canvasColour] + CID_COLOUR04 = [0x1a4, TID_SELECT, "Colour #04", "Colour #04", None, None, True, MiRCARTCanvasInterface.canvasColour] CID_COLOUR05 = [0x1a5, TID_SELECT, "Colour #05", "Colour #05", None, None, False, MiRCARTCanvasInterface.canvasColour] CID_COLOUR06 = [0x1a6, TID_SELECT, "Colour #06", "Colour #06", None, None, False, MiRCARTCanvasInterface.canvasColour] CID_COLOUR07 = [0x1a7, TID_SELECT, "Colour #07", "Colour #07", None, None, False, MiRCARTCanvasInterface.canvasColour] From d79ea00164a3f291fce38ce4ac94473d97f7bcdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Thu, 11 Jan 2018 21:28:53 +0100 Subject: [PATCH 127/148] IrcMiRCARTBot.py: include black-on-black border around map. IrcMiRCARTBot.py: normalise imported map. --- IrcMiRCARTBot.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/IrcMiRCARTBot.py b/IrcMiRCARTBot.py index 9977488..b028c43 100755 --- a/IrcMiRCARTBot.py +++ b/IrcMiRCARTBot.py @@ -142,6 +142,17 @@ class IrcMiRCARTBot(IrcClient.IrcClient): return canvasStore = MiRCARTCanvasImportStore(inFile=asciiTmpFilePath) + numRowCols = 0 + for numRow in range(len(canvasStore.outMap)): + numRowCols = max(numRowCols, len(canvasStore.outMap[numRow])) + for numRow in range(len(canvasStore.outMap)): + if len(canvasStore.outMap[numRow]) != numRowCols: + for numColOff in range(numRowCols - len(canvasStore.outMap[numRow])): + canvasStore.outMap[numRow].append([[1, 1], 0, " "]) + canvasStore.outMap[numRow].insert(0, [[1, 1], 0, " "]) + canvasStore.outMap[numRow].append([[1, 1], 0, " "]) + canvasStore.outMap.insert(0, [[[1, 1], 0, " "]] * len(canvasStore.outMap[0])) + canvasStore.outMap.append([[[1, 1], 0, " "]] * len(canvasStore.outMap[0])) MiRCARTToPngFile(canvasStore.outMap, "DejaVuSansMono.ttf", 11).export(imgTmpFilePath) imgurResponse = self._uploadToImgur(imgTmpFilePath, "MiRCART image", "MiRCART image", "c9a6efb3d7932fd") if imgurResponse[0] == 200: From 1511002798956eddbb827f0dc741ac59029d1241 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Thu, 11 Jan 2018 23:13:46 +0100 Subject: [PATCH 128/148] MiRCARTCanvasImportStore.py:_parseCharAsColourSpec(): correctly process mIRC colour control code sequences specifying one single colour. MiRCARTToPngFile.py:export(): fix indentation. --- MiRCARTCanvasImportStore.py | 2 +- MiRCARTToPngFile.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/MiRCARTCanvasImportStore.py b/MiRCARTCanvasImportStore.py index 9dc2c54..9150ea8 100644 --- a/MiRCARTCanvasImportStore.py +++ b/MiRCARTCanvasImportStore.py @@ -57,7 +57,7 @@ class MiRCARTCanvasImportStore(): return (int(colourSpec[0] or curColours[0]), \ int(colourSpec[1])) elif len(colourSpec) == 1: - return (int(colourSpec[0]), curColours[0]) + return (int(colourSpec[0]), curColours[1]) else: return (15, 1) # }}} diff --git a/MiRCARTToPngFile.py b/MiRCARTToPngFile.py index ed76ccc..de875b0 100755 --- a/MiRCARTToPngFile.py +++ b/MiRCARTToPngFile.py @@ -97,7 +97,7 @@ class MiRCARTToPngFile: else: if inCurCell[2] != " ": outColours[0] = self._ColourMapNormal[inCurCell[0][0]] - outColours[1] = self._ColourMapNormal[inCurCell[0][1]] + outColours[1] = self._ColourMapNormal[inCurCell[0][1]] outImgDraw.rectangle((*outCurPos, \ outCurPos[0] + self.outImgFontSize[0], \ outCurPos[1] + self.outImgFontSize[1]), \ From 4f4ecb4331d787e2e740bce1686133f038445701 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Thu, 11 Jan 2018 23:22:02 +0100 Subject: [PATCH 129/148] =?UTF-8?q?MiRCARTToPngFile.py:export():=20treat?= =?UTF-8?q?=20`=E2=96=88'=20as=20whitespace=20w/=20inverse=20colours.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MiRCARTToPngFile.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/MiRCARTToPngFile.py b/MiRCARTToPngFile.py index de875b0..a5f6a12 100755 --- a/MiRCARTToPngFile.py +++ b/MiRCARTToPngFile.py @@ -92,17 +92,27 @@ class MiRCARTToPngFile: outColours = [0, 0] if inCurCell[1] & 0x02: if inCurCell[2] != " ": - outColours[0] = self._ColourMapBold[inCurCell[0][0]] - outColours[1] = self._ColourMapBold[inCurCell[0][1]] + if inCurCell[2] == "█": + outColours[1] = self._ColourMapBold[inCurCell[0][0]] + else: + outColours[0] = self._ColourMapBold[inCurCell[0][0]] + outColours[1] = self._ColourMapBold[inCurCell[0][1]] + else: + outColours[1] = self._ColourMapBold[inCurCell[0][1]] else: if inCurCell[2] != " ": - outColours[0] = self._ColourMapNormal[inCurCell[0][0]] - outColours[1] = self._ColourMapNormal[inCurCell[0][1]] + if inCurCell[2] == "█": + outColours[1] = self._ColourMapNormal[inCurCell[0][0]] + else: + outColours[0] = self._ColourMapNormal[inCurCell[0][0]] + outColours[1] = self._ColourMapNormal[inCurCell[0][1]] + else: + outColours[1] = self._ColourMapNormal[inCurCell[0][1]] outImgDraw.rectangle((*outCurPos, \ outCurPos[0] + self.outImgFontSize[0], \ outCurPos[1] + self.outImgFontSize[1]), \ fill=(*outColours[1], 255)) - if inCurCell[2] != " " \ + if not inCurCell[2] in " █" \ and outColours[0] != outColours[1]: # XXX implement italic outImgDraw.text(outCurPos, \ From 802de6fa6e06cc93cbc880248d7b501475dd6ee2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Thu, 11 Jan 2018 23:26:36 +0100 Subject: [PATCH 130/148] MiRCART{CanvasInterface,Frame}.py: complete set of {brush,canvas} size operations. assets/tool{De,In}cr{Brush,Canvas}{H{,W},W}.png: added/updated. --- MiRCARTCanvasInterface.py | 48 ++++++++++++++++++++++++++++-------- MiRCARTFrame.py | 43 +++++++++++++++++++++----------- assets/toolDecrBrushH.png | Bin 0 -> 271 bytes assets/toolDecrBrushHW.png | Bin 0 -> 277 bytes assets/toolDecrBrushW.png | Bin 0 -> 250 bytes assets/toolDecrCanvasH.png | Bin 271 -> 265 bytes assets/toolDecrCanvasHW.png | Bin 0 -> 289 bytes assets/toolDecrCanvasW.png | Bin 250 -> 249 bytes assets/toolIncrBrushH.png | Bin 0 -> 236 bytes assets/toolIncrBrushHW.png | Bin 0 -> 290 bytes assets/toolIncrBrushW.png | Bin 0 -> 232 bytes assets/toolIncrCanvasH.png | Bin 236 -> 246 bytes assets/toolIncrCanvasHW.png | Bin 0 -> 282 bytes assets/toolIncrCanvasW.png | Bin 232 -> 236 bytes 14 files changed, 67 insertions(+), 24 deletions(-) create mode 100644 assets/toolDecrBrushH.png create mode 100644 assets/toolDecrBrushHW.png create mode 100644 assets/toolDecrBrushW.png create mode 100644 assets/toolDecrCanvasHW.png create mode 100644 assets/toolIncrBrushH.png create mode 100644 assets/toolIncrBrushHW.png create mode 100644 assets/toolIncrBrushW.png create mode 100644 assets/toolIncrCanvasHW.png diff --git a/MiRCARTCanvasInterface.py b/MiRCARTCanvasInterface.py index 011d45f..c6f6ed7 100644 --- a/MiRCARTCanvasInterface.py +++ b/MiRCARTCanvasInterface.py @@ -106,12 +106,21 @@ class MiRCARTCanvasInterface(): def canvasCut(self, event): pass # }}} - # {{{ canvasDecrBrush(self, event): XXX - def canvasDecrBrush(self, event): - if self.parentCanvas.brushSize[0] > 1 \ - and self.parentCanvas.brushSize[1] > 1: - self.parentCanvas.brushSize = \ - [a-1 for a in self.parentCanvas.brushSize] + # {{{ canvasDecrBrushHeight(self, event): XXX + def canvasDecrBrushHeight(self, event): + if self.parentCanvas.brushSize[1] > 1: + self.parentCanvas.brushSize[1] -= 1 + self.parentFrame.onCanvasUpdate(brushSize=self.parentCanvas.brushSize) + # }}} + # {{{ canvasDecrBrushHeightWidth(self, event): XXX + def canvasDecrBrushHeightWidth(self, event): + self.canvasDecrBrushHeight(event) + self.canvasDecrBrushWidth(event) + # }}} + # {{{ canvasDecrBrushWidth(self, event): XXX + def canvasDecrBrushWidth(self, event): + if self.parentCanvas.brushSize[0] > 1: + self.parentCanvas.brushSize[0] -= 1 self.parentFrame.onCanvasUpdate(brushSize=self.parentCanvas.brushSize) # }}} # {{{ canvasDecrCanvasHeight(self, event): XXX @@ -121,6 +130,11 @@ class MiRCARTCanvasInterface(): self.parentCanvas.canvasSize[0], \ self.parentCanvas.canvasSize[1]-1]) # }}} + # {{{ canvasDecrCanvasHeightWidth(self, event): XXX + def canvasDecrCanvasHeightWidth(self, event): + self.canvasDecrCanvasHeight(event) + self.canvasDecrCanvasWidth(event) + # }}} # {{{ canvasDecrCanvasWidth(self, event): XXX def canvasDecrCanvasWidth(self, event): if self.parentCanvas.canvasSize[0] > 1: @@ -197,10 +211,19 @@ class MiRCARTCanvasInterface(): wx.MessageBox("Failed to export to Pastebin: " + pasteResult, \ "Export to Pastebin", wx.OK|wx.ICON_EXCLAMATION) # }}} - # {{{ canvasIncrBrush(self, event): XXX - def canvasIncrBrush(self, event): - self.parentCanvas.brushSize = \ - [a+1 for a in self.parentCanvas.brushSize] + # {{{ canvasIncrBrushHeight(self, event): XXX + def canvasIncrBrushHeight(self, event): + self.parentCanvas.brushSize[1] += 1 + self.parentFrame.onCanvasUpdate(brushSize=self.parentCanvas.brushSize) + # }}} + # {{{ canvasIncrBrushHeightWidth(self, event): XXX + def canvasIncrBrushHeightWidth(self, event): + self.canvasIncrBrushHeight(event) + self.canvasIncrBrushWidth(event) + # }}} + # {{{ canvasIncrBrushWidth(self, event): XXX + def canvasIncrBrushWidth(self, event): + self.parentCanvas.brushSize[0] += 1 self.parentFrame.onCanvasUpdate(brushSize=self.parentCanvas.brushSize) # }}} # {{{ canvasIncrCanvasHeight(self, event): XXX @@ -209,6 +232,11 @@ class MiRCARTCanvasInterface(): self.parentCanvas.canvasSize[0], \ self.parentCanvas.canvasSize[1]+1]) # }}} + # {{{ canvasIncrCanvasHeightWidth(self, event): XXX + def canvasIncrCanvasHeightWidth(self, event): + self.canvasIncrCanvasHeight(event) + self.canvasIncrCanvasWidth(event) + # }}} # {{{ canvasIncrCanvasWidth(self, event): XXX def canvasIncrCanvasWidth(self, event): self._updateCanvasSize([ \ diff --git a/MiRCARTFrame.py b/MiRCARTFrame.py index 9ac8738..3a3d3bf 100644 --- a/MiRCARTFrame.py +++ b/MiRCARTFrame.py @@ -55,18 +55,30 @@ class MiRCARTFrame(MiRCARTGeneralFrame): CID_PASTE = [0x10c, TID_COMMAND, "Paste", "&Paste", ["", wx.ART_PASTE], None, False, MiRCARTCanvasInterface.canvasPaste] CID_DELETE = [0x10d, TID_COMMAND, "Delete", "De&lete", ["", wx.ART_DELETE], None, False, MiRCARTCanvasInterface.canvasDelete] CID_INCRW_CANVAS = [0x10e, TID_COMMAND, "Increase canvas width", \ - "Increase canvas &width", ["toolIncrCanvasW.png"], [wx.ACCEL_ALT, ord("D")], None, MiRCARTCanvasInterface.canvasIncrCanvasWidth] + "Increase canvas width", ["toolIncrCanvasW.png"], None, None, MiRCARTCanvasInterface.canvasIncrCanvasWidth] CID_DECRW_CANVAS = [0x10f, TID_COMMAND, "Decrease canvas width", \ - "Decrease canvas w&idth", ["toolDecrCanvasW.png"], [wx.ACCEL_ALT, ord("A")], None, MiRCARTCanvasInterface.canvasDecrCanvasWidth] + "Decrease canvas width", ["toolDecrCanvasW.png"], None, None, MiRCARTCanvasInterface.canvasDecrCanvasWidth] CID_INCRH_CANVAS = [0x110, TID_COMMAND, "Increase canvas height", \ - "Increase canvas &height", ["toolIncrCanvasH.png"], [wx.ACCEL_ALT, ord("S")], None, MiRCARTCanvasInterface.canvasIncrCanvasHeight] + "Increase canvas height", ["toolIncrCanvasH.png"], None, None, MiRCARTCanvasInterface.canvasIncrCanvasHeight] CID_DECRH_CANVAS = [0x111, TID_COMMAND, "Decrease canvas height", \ - "Decrease canvas h&eight", ["toolDecrCanvasH.png"], [wx.ACCEL_ALT, ord("W")], None, MiRCARTCanvasInterface.canvasDecrCanvasHeight] - CID_INCR_BRUSH = [0x112, TID_COMMAND, "Increase brush size", \ - "I&ncrease brush size", ["", wx.ART_PLUS], [wx.ACCEL_CTRL, ord("+")], None, MiRCARTCanvasInterface.canvasIncrBrush] - CID_DECR_BRUSH = [0x113, TID_COMMAND, "Decrease brush size", \ - "&Decrease brush size", ["", wx.ART_MINUS], [wx.ACCEL_CTRL, ord("-")], None, MiRCARTCanvasInterface.canvasDecrBrush] - CID_SOLID_BRUSH = [0x114, TID_SELECT, "Solid brush", "&Solid brush", None, None, True, MiRCARTCanvasInterface.canvasBrushSolid] + "Decrease canvas height", ["toolDecrCanvasH.png"], None, None, MiRCARTCanvasInterface.canvasDecrCanvasHeight] + CID_INCRHW_CANVAS = [0x112, TID_COMMAND, "Increase canvas size", \ + "Increase canvas size", ["toolIncrCanvasHW.png"], None, None, MiRCARTCanvasInterface.canvasIncrCanvasHeightWidth] + CID_DECRHW_CANVAS = [0x113, TID_COMMAND, "Decrease canvas size", \ + "Decrease canvas size", ["toolDecrCanvasHW.png"], None, None, MiRCARTCanvasInterface.canvasDecrCanvasHeightWidth] + CID_INCRW_BRUSH = [0x114, TID_COMMAND, "Increase brush width", \ + "Increase brush width", ["toolIncrBrushW.png"], None, None, MiRCARTCanvasInterface.canvasIncrBrushWidth] + CID_DECRW_BRUSH = [0x115, TID_COMMAND, "Decrease brush width", \ + "Decrease brush width", ["toolDecrBrushW.png"], None, None, MiRCARTCanvasInterface.canvasDecrBrushWidth] + CID_INCRH_BRUSH = [0x116, TID_COMMAND, "Increase brush height", \ + "Increase brush height", ["toolIncrBrushH.png"], None, None, MiRCARTCanvasInterface.canvasIncrBrushHeight] + CID_DECRH_BRUSH = [0x117, TID_COMMAND, "Decrease brush height", \ + "Decrease brush height", ["toolDecrBrushH.png"], None, None, MiRCARTCanvasInterface.canvasDecrBrushHeight] + CID_INCRHW_BRUSH = [0x118, TID_COMMAND, "Increase brush size", \ + "Increase brush size", ["toolIncrBrushHW.png"], None, None, MiRCARTCanvasInterface.canvasIncrBrushHeightWidth] + CID_DECRHW_BRUSH = [0x119, TID_COMMAND, "Decrease brush size", \ + "Decrease brush size", ["toolDecrBrushHW.png"], None, None, MiRCARTCanvasInterface.canvasDecrBrushHeightWidth] + CID_SOLID_BRUSH = [0x11a, TID_SELECT, "Solid brush", "Solid brush", None, None, True, MiRCARTCanvasInterface.canvasBrushSolid] CID_RECT = [0x150, TID_SELECT, "Rectangle", "&Rectangle", ["toolRect.png"], [wx.ACCEL_CTRL, ord("R")], True, MiRCARTCanvasInterface.canvasToolRect] CID_CIRCLE = [0x151, TID_SELECT, "Circle", "&Circle", ["toolCircle.png"], [wx.ACCEL_CTRL, ord("C")], False, MiRCARTCanvasInterface.canvasToolCircle] @@ -102,7 +114,10 @@ class MiRCARTFrame(MiRCARTGeneralFrame): CID_UNDO, CID_REDO, NID_MENU_SEP, \ CID_CUT, CID_COPY, CID_PASTE, CID_DELETE, NID_MENU_SEP, \ CID_INCRW_CANVAS, CID_DECRW_CANVAS, CID_INCRH_CANVAS, CID_DECRH_CANVAS, NID_MENU_SEP, \ - CID_INCR_BRUSH, CID_DECR_BRUSH, CID_SOLID_BRUSH)) + CID_INCRHW_CANVAS, CID_DECRHW_CANVAS, NID_MENU_SEP, \ + CID_INCRW_BRUSH, CID_DECRW_BRUSH, CID_INCRH_BRUSH, CID_DECRH_BRUSH, NID_MENU_SEP, \ + CID_INCRHW_BRUSH, CID_DECRHW_BRUSH, NID_MENU_SEP, \ + CID_SOLID_BRUSH)) MID_TOOLS = (0x302, TID_MENU, "Tools", "&Tools", ( \ CID_RECT, CID_CIRCLE, CID_FILL, CID_LINE, CID_TEXT, CID_CLONE_SELECT, CID_MOVE_SELECT)) # }}} @@ -111,20 +126,20 @@ class MiRCARTFrame(MiRCARTGeneralFrame): CID_NEW, CID_OPEN, CID_SAVE, CID_SAVEAS, NID_TOOLBAR_HSEP, \ CID_UNDO, CID_REDO, NID_TOOLBAR_HSEP, \ CID_CUT, CID_COPY, CID_PASTE, CID_DELETE, NID_TOOLBAR_HSEP, \ - CID_INCR_BRUSH, CID_DECR_BRUSH, NID_TOOLBAR_HSEP, \ CID_INCRW_CANVAS, CID_DECRW_CANVAS, CID_INCRH_CANVAS, CID_DECRH_CANVAS, NID_TOOLBAR_HSEP, \ + CID_INCRHW_CANVAS, CID_DECRHW_CANVAS, NID_TOOLBAR_HSEP, \ CID_RECT, CID_CIRCLE, CID_FILL, CID_LINE, CID_TEXT, CID_CLONE_SELECT, CID_MOVE_SELECT, \ NID_TOOLBAR_VSEP, \ CID_COLOUR00, CID_COLOUR01, CID_COLOUR02, CID_COLOUR03, CID_COLOUR04, \ CID_COLOUR05, CID_COLOUR06, CID_COLOUR07, CID_COLOUR08, CID_COLOUR09, \ CID_COLOUR10, CID_COLOUR11, CID_COLOUR12, CID_COLOUR13, CID_COLOUR14, \ - CID_COLOUR15)) + CID_COLOUR15, NID_TOOLBAR_HSEP, \ + CID_INCRW_BRUSH, CID_DECRW_BRUSH, CID_INCRH_BRUSH, CID_DECRH_BRUSH, NID_TOOLBAR_HSEP, \ + CID_INCRHW_BRUSH, CID_DECRHW_BRUSH)) # }}} # {{{ Accelerators (hotkeys) AID_EDIT = (0x500, TID_ACCELS, ( \ CID_NEW, CID_OPEN, CID_SAVE, CID_EXIT, CID_UNDO, CID_REDO, \ - CID_INCRW_CANVAS, CID_DECRW_CANVAS, CID_INCRH_CANVAS, CID_DECRH_CANVAS, \ - CID_INCR_BRUSH, CID_DECR_BRUSH, \ CID_RECT, CID_CIRCLE, CID_FILL, CID_LINE, CID_TEXT, CID_CLONE_SELECT, CID_MOVE_SELECT)) # }}} # {{{ Lists diff --git a/assets/toolDecrBrushH.png b/assets/toolDecrBrushH.png new file mode 100644 index 0000000000000000000000000000000000000000..6b372738c70ceac3d8eb7dfb8d14e1aecd9592be GIT binary patch literal 271 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DkxK$!8B)5ZfpLCF%=h?3y^w370~qEv>0#LT=By}Z;C1rt33JwwYY*EXd974>+! zIEGmGFYR&UJ8ZzgGTE#>PV4Fa`KlXC-$_jfUOmmNEiyrP;(m>%T#*jFdB<*KyxuVX zqwcMsKkrW{>1_KR!l!NFJkO=}!O|NO-&N0Jk=?NCQ1O`pW=H0I0v~0J?>HQ3Ja0X9 zyXYK6|3!8iwE2Om?{#mJ_#gJT@_bM4Hoo7IkvclkOJf#J*{^(?HHWADxK5Y(A)vb$ NJYD@<);T3K0RX`gV}t+z literal 0 HcmV?d00001 diff --git a/assets/toolDecrBrushHW.png b/assets/toolDecrBrushHW.png new file mode 100644 index 0000000000000000000000000000000000000000..b5c14cab292f3c54f7e4ca27ef5d452646aa4102 GIT binary patch literal 277 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjGL736~_k^`TLCF%=h?3y^w370~qEv>0#LT=By}Z;C1rt33JwwYY*EXd96;1MV zaSXBWU)y_-@2~-f^XAg_IJK|;>&==O`X(<-nfyy7*g#EpYnVnt{_?_t8@E@QpS$zk z#NpX$Nv3vVMU^d!4Sw#PB$_mDu5*k0hGUA{63ad0f8JX>@tgeJ-weXWOvhBKvW4xw ziEzn92^d_d&H}MtEo*FVy0LkU#l6>w)?pTNPI65=s~s6>Ab-Z|?ETQw>@(G#r^?M@ R)B(DV!PC{xWt~$(698I$W1Rp1 literal 0 HcmV?d00001 diff --git a/assets/toolDecrBrushW.png b/assets/toolDecrBrushW.png new file mode 100644 index 0000000000000000000000000000000000000000..64e964b41c9004f4b29f1a2f3d913cd380de3c9a GIT binary patch literal 250 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DkxK$!8B)5ZfpLCF%=h?3y^w370~qEv>0#LT=By}Z;C1rt33JwwYY*EXd96_tCs zIEGmGFYO8BYcSw&?zKO*;=8@xI&PoFT^6d9o|PrDU7m=C^f^4`h-+@SY6GAX7(8A5T-G@yGywpyLR4%3 literal 0 HcmV?d00001 diff --git a/assets/toolDecrCanvasH.png b/assets/toolDecrCanvasH.png index 6b372738c70ceac3d8eb7dfb8d14e1aecd9592be..1ea55ef3699cd94dedc187666d5852c949a77999 100644 GIT binary patch delta 177 zcmV;i08amp0*L~UJPN%401mwYNBfYikwz$ghDk(0R5(wij7tu{AP7Y5XVFcQ-v6?e zG2jQ0XiM^R7zP4W{7HyZc$P5_=-va_W`T`_cpjXE9{(Q5bU~v>9mENtX43~Q0!OmX zGyedzg_;*b%vxKhc`?LPJ?bHD2sN8Nun{|inw5po&pMU#mEQ&NB*gRJEOcwuy$7-! fOADHO;5#9{-ZT(D8=bp~00000NkvXXu0mjfAel?w delta 183 zcmV;o07(Cd0*?ZaJPN)501mzZ>O!%=kwz$gj7da6R5(wij6n{=FbD*bHiup+<^P{0 zur=-)kW;IcMuu5n6Oq3t!lvI6;>1s;*b z3=DjGK$vmro+3}6pk#?_L`iUdT1k0gQ7S`0VrE{6US4X6f{C7io}uNHYnxJlispH` zIEGmGZ=DdxcgTRp+1jB)Ugcf=%}M5SCX_|Dn@m_4XKXtA zaY=dMg~=W~6MA;4v|sm7X?r-2XG3(;S|6FlA1y7bKOXy3?8w#FK5^28r@CAbWk2UL zah`p>`(bL$rX70ktz}+ny#J#wJ}vNIltSDD+n4hf%I~#Q&bHk+HIrXP?v9e+Qzwoc hW=e+g?2)ge%tJPN%401mwYNBfYikwz$gc1c7*R5(wyj8O`JFbqS7KO*Y=k2_mP zN7tx$)yh>$?-$M?82!c z$=nvKfd0sELA*VqY-f6%5%bN!@fa#F!*oW>cZQk%_KdP*&d>_pQ|bk?trJIU2~~Ft P00000NkvXXu0mjf`36Sc delta 162 zcmV;T0A2t20r~-uJPN)501mzZ>O!%=kwz$gcS%G+R5(wij8P7NFbG49KgOu{KQ68d zOo6&MC3#7Cbelxv7gUZw!lvI6;>1s;*b z3=DkxK$!8B)5ZfpLCF%=h?3y^w370~qEv>0#LT=By}Z;C1rt33JwwYY*EXd96=iz5 zIEGmGukDHEJD|Y9viJYel@sz#t&Z3tFMMZWs=!6Tm2U)EHC}7w%n;0o|7i1W(tPbj z9$jzl@wt6`&M0F0W4f%Y%L-Nz8!%%B>y9(Vfp6pvG|krrYY=o@d}hfeXRZCtkC`es W*5-#79$yQzl)=;0&t;ucLK6UCCs9ZM literal 0 HcmV?d00001 diff --git a/assets/toolIncrBrushHW.png b/assets/toolIncrBrushHW.png new file mode 100644 index 0000000000000000000000000000000000000000..34f3a41458da9ff15f619a2ae06c72a7081d565f GIT binary patch literal 290 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjGL736~_k^`TLCF%=h?3y^w370~qEv>0#LT=By}Z;C1rt33JwwYY*EXd970vf_ zaSXBWU)y(^>#zZj^X}7!=M_oZ8$K==iX-qnPr!h6)IT%GM~FKr7`EE_K67(WsUb*v`_z$-{5VV z&%<8*q3xjSLdWa6{J*$7-t!%vW_Z|f#q9PXPsR!N=008!@m%SjQVRobd?C=J|8fDL g;#$+6S>^s__|kqizrE;%GSI0Ep00i_>zopr0C^Q|mjD0& literal 0 HcmV?d00001 diff --git a/assets/toolIncrBrushW.png b/assets/toolIncrBrushW.png new file mode 100644 index 0000000000000000000000000000000000000000..6790bfc4babf737cd7a644743ccb6e1109550703 GIT binary patch literal 232 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DkxL735kHCP2GC|TkfQ4*Y=R#Ki=l*&+$n3-3imzP?iV4`QBXJ~ol+NKnsqEt^8 z#}EturM--N2NXD%|NdV-b4J3F5{nrpC3iCM3I1fAD!O8g!&1ZkxX&kxTfG+VJ-{aT zCb%g=h>aIWS)63!)#|<0l+%#9Z^xfy3H{t!Ev&08n#?z(+i&xjw&9GZ?212qJ?vA? Wtj!nQwL=M%9u{#6d+00009 M07*qoM6N<$g6gqKc>n+a delta 147 zcmeyy_=a(U4c9&f2EKiaubeg>nCPZam+9%^7-HeSwkMkJfC2~0-v3KiPRKj8I%12w z@STaN0v82Wz7c5Ec&(K)Log%$qs_ZX^R*XwbiKL9=l1bAqloQ~>9VpeD_BKrz>FQN xJI)vfzL7i7G+!I6LC|&anI)T?we~wdW~$&=n;%|yd@TbIc)I$ztaD0e0su1SJ^TOw diff --git a/assets/toolIncrCanvasHW.png b/assets/toolIncrCanvasHW.png new file mode 100644 index 0000000000000000000000000000000000000000..4002cc8bd198d614cb9a4f65d268aec5107f8796 GIT binary patch literal 282 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjGK$vmro+3}6pk#?_L`iUdT1k0gQ7S`0VrE{6US4X6f{C7io}uNHYnxJlil%$I zIEGmGFAd`4Yf#{E<~{bmUgCOWLBoqHYO>b5*qR+D)ETKT`mq-2D?UG$snEd@W}%d# zDll1+$z#cz>5k{R4<38AR&t&48NF|{z|jfF`G5CrmOB#Qn?c9+xoVbBa%;E@)95O XgTe~DWM4fdjwwf literal 0 HcmV?d00001 diff --git a/assets/toolIncrCanvasW.png b/assets/toolIncrCanvasW.png index 6790bfc4babf737cd7a644743ccb6e1109550703..48f8053c6a218392aa207d1a6f2e008344bdb116 100644 GIT binary patch delta 147 zcmaFC_=a(U4c9&f2EKiaubeg>nCPZam+9%^7-HeSwTF@GfB^^d-~Y?!Hi@m3y45SX zZpq2ONr&YXr!Bs2`oBJ*$z?}&8;HnjKlGs2Iq}1*2Qw4|-_2SGB8nyNPdw+FdPaEn zey(bnV7m{8J|B6v{LtwieIbP0l+XkKe~d*9 delta 143 zcmV;A0C4~80q6mcJPN-601m$aI0aKAkwz$bWl2OqR5(wijR6k8AP58f|EHOlV5D?7 zn8_Kr0uB-R1(g-3tU#qPk6!c1bA?Kyy}$+$>{EhR5(W+IQ-V0j1`R5W)`Dz+Wxlxl xreKc?Due~AID$8@XFs+`maxnf9jN?{j0cp=t#1{&xElZf002ovPDHLkV1m<)Iqm=e From 0cf1040614ee3073262a3d84582f336544946903 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Thu, 11 Jan 2018 23:27:57 +0100 Subject: [PATCH 131/148] MiRCARTColours.py: fix permission bits. --- MiRCARTColours.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 MiRCARTColours.py diff --git a/MiRCARTColours.py b/MiRCARTColours.py old mode 100755 new mode 100644 From b385909b45dad44567ee7b6b7bdee2858b932e2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Sat, 20 Jan 2018 09:17:52 +0100 Subject: [PATCH 132/148] MiRCARTCanvasImportStore.py: correctly process ^C, sequences. MiRCARTToPngFile.py: fix {bold,underline} processing. --- MiRCARTCanvasImportStore.py | 19 ++++++++++++++----- MiRCARTToPngFile.py | 28 +++++++++++++++------------- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/MiRCARTCanvasImportStore.py b/MiRCARTCanvasImportStore.py index 9150ea8..e35b4a9 100644 --- a/MiRCARTCanvasImportStore.py +++ b/MiRCARTCanvasImportStore.py @@ -53,10 +53,12 @@ class MiRCARTCanvasImportStore(): def _parseCharAsColourSpec(self, colourSpec, curColours): if len(colourSpec) > 0: colourSpec = colourSpec.split(",") - if len(colourSpec) == 2: + if len(colourSpec) == 2 \ + and len(colourSpec[1]) > 0: return (int(colourSpec[0] or curColours[0]), \ int(colourSpec[1])) - elif len(colourSpec) == 1: + elif len(colourSpec) == 1 \ + or len(colourSpec[1]) == 0: return (int(colourSpec[0]), curColours[1]) else: return (15, 1) @@ -108,9 +110,16 @@ class MiRCARTCanvasImportStore(): or inParseState == self._ParseState.PS_COLOUR_DIGIT1: if inChar == "," \ and inParseState == self._ParseState.PS_COLOUR_DIGIT0: - inCurCol += 1 - inCurColourDigits = 0; inCurColourSpec += inChar; - inParseState = self._ParseState.PS_COLOUR_DIGIT1 + if (inCurCol + 1) < inMaxCol \ + and not inLine[inCurCol + 1] in set("0123456789"): + inCurColours = self._parseCharAsColourSpec( \ + inCurColourSpec, inCurColours) + inCurColourDigits = 0; inCurColourSpec = ""; + inParseState = self._ParseState.PS_CHAR + else: + inCurCol += 1 + inCurColourDigits = 0; inCurColourSpec += inChar; + inParseState = self._ParseState.PS_COLOUR_DIGIT1 elif inChar in set("0123456789") \ and inCurColourDigits == 0: inCurCol += 1 diff --git a/MiRCARTToPngFile.py b/MiRCARTToPngFile.py index a5f6a12..16661b8 100755 --- a/MiRCARTToPngFile.py +++ b/MiRCARTToPngFile.py @@ -71,13 +71,12 @@ class MiRCARTToPngFile: [187, 187, 187], # Light Grey ] # }}} - # {{{ _drawUnderline(self, curPos, fontSize, imgDraw): XXX - def _drawUnderLine(self, curPos, fontSize, imgDraw): - imgDraw.line( \ - (curPos[0], \ - curPos[1] + (fontSize[1] - 2)), \ - (curPos[0] + fontSize[0], \ - curPos[1] + (fontSize[1] - 2)), fill=outColours[0]) + # {{{ _drawUnderline(self, curPos, fontSize, imgDraw, fillColour): XXX + def _drawUnderLine(self, curPos, fontSize, imgDraw, fillColour): + imgDraw.line( \ + xy=(curPos[0], curPos[1] + (fontSize[1] - 2), \ + curPos[0] + fontSize[0], curPos[1] + (fontSize[1] - 2)), \ + fill=fillColour) # }}} # {{{ export(self, outFilePath): XXX def export(self, outFilePath): @@ -90,15 +89,15 @@ class MiRCARTToPngFile: for inCurCol in range(len(self.inCanvasMap[inCurRow])): inCurCell = self.inCanvasMap[inCurRow][inCurCol] outColours = [0, 0] - if inCurCell[1] & 0x02: + if inCurCell[1] & MiRCARTCanvasImportStore.MiRCARTCanvasImportStore._CellState.CS_BOLD: if inCurCell[2] != " ": if inCurCell[2] == "█": - outColours[1] = self._ColourMapBold[inCurCell[0][0]] + outColours[1] = self._ColourMapNormal[inCurCell[0][0]] else: outColours[0] = self._ColourMapBold[inCurCell[0][0]] - outColours[1] = self._ColourMapBold[inCurCell[0][1]] + outColours[1] = self._ColourMapNormal[inCurCell[0][1]] else: - outColours[1] = self._ColourMapBold[inCurCell[0][1]] + outColours[1] = self._ColourMapNormal[inCurCell[0][1]] else: if inCurCell[2] != " ": if inCurCell[2] == "█": @@ -117,8 +116,11 @@ class MiRCARTToPngFile: # XXX implement italic outImgDraw.text(outCurPos, \ inCurCell[2], (*outColours[0], 255), self.outImgFont) - if inCurCell[1] & 0x1f: - self._drawUnderLine(curPos, self.outImgFontSize, outImgDraw) + if inCurCell[1] & MiRCARTCanvasImportStore.MiRCARTCanvasImportStore._CellState.CS_UNDERLINE: + outColours[0] = self._ColourMapNormal[inCurCell[0][0]] + self._drawUnderLine(outCurPos, \ + self.outImgFontSize, \ + outImgDraw, (*outColours[0], 255)) outCurPos[0] += self.outImgFontSize[0]; outCurPos[0] = 0 outCurPos[1] += self.outImgFontSize[1] From f77e6d5bf82a52a721d8401b4ae6e0635d033345 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Mon, 22 Jan 2018 21:06:46 +0100 Subject: [PATCH 133/148] IrcClient.py:connect(): create file from socket w/ errors="replace" to prevent UnicodeDecodeErrors. --- IrcClient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IrcClient.py b/IrcClient.py index 1d8647a..4891027 100644 --- a/IrcClient.py +++ b/IrcClient.py @@ -52,7 +52,7 @@ class IrcClient: self.close(); return False; else: select.select([], [self.clientSocket.fileno()], []) - self.clientSocketFile = self.clientSocket.makefile(encoding="utf-8") + self.clientSocketFile = self.clientSocket.makefile(encoding="utf-8", errors="replace") self.clientQueue = [] self.queue("NICK", self.clientNick) self.queue("USER", self.clientIdent, "0", "0", self.clientGecos) From 92124dc34f49e843831b0ddf464cd358c0dcc789 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Thu, 25 Jan 2018 15:02:09 +0100 Subject: [PATCH 134/148] MiRCARTCanvas.py:resize(): clean up & fix. MiRCARTCanvasInterface.py:_updateCanvasSize(): merged into ...Canvas.resize(). MiRCARTCanvasInterface.py:canvas{De,In}crCanvas{Height,Width}(): directly call ...Canvas.resize(). --- MiRCARTCanvas.py | 50 +++++++++++++++++++++++++------------ MiRCARTCanvasInterface.py | 52 +++++---------------------------------- 2 files changed, 40 insertions(+), 62 deletions(-) diff --git a/MiRCARTCanvas.py b/MiRCARTCanvas.py index 0d5912c..c62e853 100644 --- a/MiRCARTCanvas.py +++ b/MiRCARTCanvas.py @@ -137,30 +137,48 @@ class MiRCARTCanvas(wx.Panel): def resize(self, newCanvasSize): if newCanvasSize != self.canvasSize: if self.canvasMap == None: - self.canvasMap = [[[(1, 1), 0, " "] \ - for x in range(self.canvasSize[0])] \ - for y in range(self.canvasSize[1])] + self.canvasMap = []; oldCanvasSize = [0, 0]; else: - for numRow in range(self.canvasSize[1]): - for numNewCol in range(self.canvasSize[0], newCanvasSize[0]): - self.canvasMap[numRow].append([[1, 1], 0, " "]) - for numNewRow in range(self.canvasSize[1], newCanvasSize[1]): - self.canvasMap.append([]) - for numNewCol in range(newCanvasSize[0]): - self.canvasMap[numNewRow].append([[1, 1], 0, " "]) - self.canvasSize = newCanvasSize - newWinSize = [a*b for a,b in \ - zip(self.canvasSize, self.canvasBackend.cellSize)] + oldCanvasSize = self.canvasSize + deltaCanvasSize = [b-a for a,b in zip(oldCanvasSize, newCanvasSize)] + + newWinSize = [a*b for a,b in zip(newCanvasSize, self.canvasBackend.cellSize)] self.SetMinSize(newWinSize) self.SetSize(wx.DefaultCoord, wx.DefaultCoord, *newWinSize) curWindow = self while curWindow != None: curWindow.Layout() curWindow = curWindow.GetParent() - self.canvasBackend.reset(self.canvasSize, self.canvasBackend.cellSize) + + self.canvasBackend.resize(newCanvasSize, self.canvasBackend.cellSize) + eventDc = self.canvasBackend.getDeviceContext(self) self.canvasJournal.resetCursor(); self.canvasJournal.resetUndo(); - self.parentFrame.onCanvasUpdate( \ - size=self.canvasSize, undoLevel=-1) + + if deltaCanvasSize[0] < 0: + for numRow in range(oldCanvasSize[1]): + del self.canvasMap[numRow][-1:(deltaCanvasSize[0]-1):-1] + else: + for numRow in range(oldCanvasSize[1]): + self.canvasMap[numRow].extend( \ + [[[1, 1], 0, " "]] * deltaCanvasSize[0]) + for numNewCol in range(oldCanvasSize[0], newCanvasSize[0]): + self.canvasBackend.drawPatch( \ + eventDc, [[numNewCol, numRow], \ + *self.canvasMap[numRow][-1]]) + if deltaCanvasSize[1] < 0: + del self.canvasMap[-1:(deltaCanvasSize[1]-1):-1] + else: + for numNewRow in range(oldCanvasSize[1], newCanvasSize[1]): + self.canvasMap.extend( \ + [[[[1, 1], 0, " "]] * newCanvasSize[0]]) + for numNewCol in range(newCanvasSize[0]): + self.canvasBackend.drawPatch( \ + eventDc, [[numNewCol, numNewRow], \ + *self.canvasMap[-1][-1]]) + + self.canvasSize = newCanvasSize + wx.SafeYield() + self.parentFrame.onCanvasUpdate(size=newCanvasSize, undoLevel=-1) # }}} # diff --git a/MiRCARTCanvasInterface.py b/MiRCARTCanvasInterface.py index c6f6ed7..d546e1d 100644 --- a/MiRCARTCanvasInterface.py +++ b/MiRCARTCanvasInterface.py @@ -45,46 +45,6 @@ class MiRCARTCanvasInterface(): dialogChoice = dialog.ShowModal() return dialogChoice # }}} - # {{{ _updateCanvasSize(self, newCanvasSize): XXX - def _updateCanvasSize(self, newCanvasSize): - eventDc = self.parentCanvas.canvasBackend.getDeviceContext(self.parentCanvas) - self.parentCanvas.canvasBackend.drawCursorMaskWithJournal( \ - self.parentCanvas.canvasJournal, eventDc) - oldCanvasSize = self.parentCanvas.canvasSize - self.parentCanvas.resize(newCanvasSize) - self.parentCanvas.canvasBackend.resize( \ - newCanvasSize, \ - self.parentCanvas.canvasBackend.cellSize) - if (newCanvasSize[1] - oldCanvasSize[1]) < 0: - for numRowOff in range(1, (oldCanvasSize[1] - newCanvasSize[1]) + 1): - numRow = oldCanvasSize[1] - numRowOff - del self.parentCanvas.canvasMap[numRow] - else: - for numRowOff in range(oldCanvasSize[1] - newCanvasSize[1]): - numRow = oldCanvasSize[1] + numRowOff - self.parentCanvas.canvasMap.append(None) - self.parentCanvas.canvasMap[numRow] = \ - [[[1, 1], 0, " "]] * oldCanvasSize[0] - self.parentCanvas.canvasBackend.drawPatch( \ - eventDc, \ - [[numCol, numRow], *[[1, 1], 0, " "]]) - if (newCanvasSize[0] - oldCanvasSize[0]) < 0: - for numRow in range(newCanvasSize[1]): - for numColOff in range(1, (oldCanvasSize[0] - newCanvasSize[0]) + 1): - numCol = oldCanvasSize[0] - numColOff - del self.parentCanvas.canvasMap[numRow][numCol] - else: - for numRow in range(newCanvasSize[1]): - for numColOff in range(newCanvasSize[0] - oldCanvasSize[0]): - numCol = oldCanvasSize[0] + numColOff - self.parentCanvas.canvasMap[numRow].append(None) - self.parentCanvas.canvasMap[numRow][numCol] = \ - [[1, 1], 0, " "] - self.parentCanvas.canvasBackend.drawPatch( \ - eventDc, \ - [[numCol, numRow], *[[1, 1], 0, " "]]) - wx.SafeYield() - # }}} # {{{ canvasBrushSolid(self, event): XXX def canvasBrushSolid(self, event): @@ -126,8 +86,8 @@ class MiRCARTCanvasInterface(): # {{{ canvasDecrCanvasHeight(self, event): XXX def canvasDecrCanvasHeight(self, event): if self.parentCanvas.canvasSize[1] > 1: - self._updateCanvasSize([ \ - self.parentCanvas.canvasSize[0], \ + self.parentCanvas.resize([ \ + self.parentCanvas.canvasSize[0], \ self.parentCanvas.canvasSize[1]-1]) # }}} # {{{ canvasDecrCanvasHeightWidth(self, event): XXX @@ -138,8 +98,8 @@ class MiRCARTCanvasInterface(): # {{{ canvasDecrCanvasWidth(self, event): XXX def canvasDecrCanvasWidth(self, event): if self.parentCanvas.canvasSize[0] > 1: - self._updateCanvasSize([ \ - self.parentCanvas.canvasSize[0]-1, \ + self.parentCanvas.resize([ \ + self.parentCanvas.canvasSize[0]-1, \ self.parentCanvas.canvasSize[1]]) # }}} # {{{ canvasDelete(self, event): XXX @@ -228,7 +188,7 @@ class MiRCARTCanvasInterface(): # }}} # {{{ canvasIncrCanvasHeight(self, event): XXX def canvasIncrCanvasHeight(self, event): - self._updateCanvasSize([ \ + self.parentCanvas.resize([ \ self.parentCanvas.canvasSize[0], \ self.parentCanvas.canvasSize[1]+1]) # }}} @@ -239,7 +199,7 @@ class MiRCARTCanvasInterface(): # }}} # {{{ canvasIncrCanvasWidth(self, event): XXX def canvasIncrCanvasWidth(self, event): - self._updateCanvasSize([ \ + self.parentCanvas.resize([ \ self.parentCanvas.canvasSize[0]+1, \ self.parentCanvas.canvasSize[1]]) # }}} From 579dd858ba04589bd14f34fd386fa02a1eb61976 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Thu, 25 Jan 2018 15:04:37 +0100 Subject: [PATCH 135/148] MiRCARTCanvasInterface.py:canvasSave(): fix call parameters. --- MiRCARTCanvasInterface.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/MiRCARTCanvasInterface.py b/MiRCARTCanvasInterface.py index d546e1d..c0543be 100644 --- a/MiRCARTCanvasInterface.py +++ b/MiRCARTCanvasInterface.py @@ -115,7 +115,7 @@ class MiRCARTCanvasInterface(): elif saveChanges == wx.ID_NO: pass elif saveChanges == wx.ID_YES: - self.canvasSave() + self.canvasSave(event) self.parentFrame.Close(True) # }}} # {{{ canvasExportAsPng(self, event): XXX @@ -212,7 +212,7 @@ class MiRCARTCanvasInterface(): elif saveChanges == wx.ID_NO: pass elif saveChanges == wx.ID_YES: - self.canvasSave() + self.canvasSave(event) self.parentCanvas.SetCursor(wx.Cursor(wx.CURSOR_WAIT)) if newCanvasSize == None: newCanvasSize = list(self.parentCanvas.defaultCanvasSize) @@ -230,7 +230,7 @@ class MiRCARTCanvasInterface(): elif saveChanges == wx.ID_NO: pass elif saveChanges == wx.ID_YES: - self.canvasSave() + self.canvasSave(event) with wx.FileDialog(self.parentCanvas, "Open", os.getcwd(), "", \ "*.txt", wx.FD_OPEN) as dialog: if dialog.ShowModal() == wx.ID_CANCEL: From c6ab27c08caa7039e08b73fdaf803f244aef8d46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Fri, 26 Jan 2018 17:45:04 +0100 Subject: [PATCH 136/148] Fixes memory leaks on {re,un}do state mutation & resize-related operations. MiRCARTCanvas.py:__del__(): delete canvasMap w/ clear(). MiRCARTCanvasBackend.py:resize(): delete old canvasBitmap w/ Destroy(). MiRCARTCanvasJournal.py:reset{Cursor,Undo}(): delete patches{Cursor,Undo} w/ clear(). MiRCARTCanvasJournal.py:__del__(): provided explicitly to call reset{Cursor,Undo}(). MiRCARTFrame.py:__del__(): provided explicitly to delete panelCanvas. --- MiRCARTCanvas.py | 8 +++++++- MiRCARTCanvasBackend.py | 5 +++-- MiRCARTCanvasJournal.py | 9 +++++++++ MiRCARTFrame.py | 6 ++++++ 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/MiRCARTCanvas.py b/MiRCARTCanvas.py index c62e853..b75af72 100644 --- a/MiRCARTCanvas.py +++ b/MiRCARTCanvas.py @@ -181,8 +181,14 @@ class MiRCARTCanvas(wx.Panel): self.parentFrame.onCanvasUpdate(size=newCanvasSize, undoLevel=-1) # }}} + # {{{ __del__(self): destructor method + def __del__(self): + if self.canvasMap != None: + self.canvasMap.clear(); self.canvasMap = None; + # }}} + # - # _init__(self, parent, parentFrame, defaultCanvasPos, defaultCanvasSize, defaultCellSize): initialisation method + # __init__(self, parent, parentFrame, defaultCanvasPos, defaultCanvasSize, defaultCellSize): initialisation method def __init__(self, parent, parentFrame, defaultCanvasPos, defaultCanvasSize, defaultCellSize): super().__init__(parent, pos=defaultCanvasPos, \ size=[w*h for w,h in zip(defaultCanvasSize, defaultCellSize)]) diff --git a/MiRCARTCanvasBackend.py b/MiRCARTCanvasBackend.py index 154b0ca..f7b20e2 100644 --- a/MiRCARTCanvasBackend.py +++ b/MiRCARTCanvasBackend.py @@ -141,7 +141,8 @@ class MiRCARTCanvasBackend(): newBitmap = wx.Bitmap(winSize) newDc.SelectObject(newBitmap) newDc.Blit(0, 0, *self.canvasBitmap.GetSize(), oldDc, 0, 0) - self.canvasBitmap = newBitmap + oldDc.SelectObject(wx.NullBitmap) + self.canvasBitmap.Destroy(); self.canvasBitmap = newBitmap; self.canvasSize = canvasSize; self.cellSize = cellSize; self._font = wx.Font( \ 8, \ @@ -166,7 +167,7 @@ class MiRCARTCanvasBackend(): # }}} # - # _init__(self, canvasSize, cellSize): initialisation method + # __init__(self, canvasSize, cellSize): initialisation method def __init__(self, canvasSize, cellSize): self._initBrushesAndPens() self.reset(canvasSize, cellSize) diff --git a/MiRCARTCanvasJournal.py b/MiRCARTCanvasJournal.py index d487a60..6bbaa81 100644 --- a/MiRCARTCanvasJournal.py +++ b/MiRCARTCanvasJournal.py @@ -68,10 +68,14 @@ class MiRCARTCanvasJournal(): # }}} # {{{ resetCursor(self): XXX def resetCursor(self): + if self.patchesCursor != None: + self.patchesCursor.clear() self.patchesCursor = [] # }}} # {{{ resetUndo(self): XXX def resetUndo(self): + if self.patchesUndo != None: + self.patchesUndo.clear() self.patchesUndo = [None]; self.patchesUndoLevel = 0; # }}} # {{{ updateCurrentDeltas(self, undoPatches, redoPatches): XXX @@ -80,6 +84,11 @@ class MiRCARTCanvasJournal(): self.patchesUndo[0][1].append(redoPatches) # }}} + # {{{ __del__(self): destructor method + def __del__(self): + self.resetCursor(); self.resetUndo(); + # }}} + # # __init__(self): initialisation method def __init__(self): diff --git a/MiRCARTFrame.py b/MiRCARTFrame.py index 3a3d3bf..4c929dc 100644 --- a/MiRCARTFrame.py +++ b/MiRCARTFrame.py @@ -227,6 +227,12 @@ class MiRCARTFrame(MiRCARTGeneralFrame): toolBar.EnableTool(self.CID_REDO[0], False) # }}} + # {{{ __del__(self): destructor method + def __del__(self): + if self.panelCanvas != None: + del self.panelCanvas; self.panelCanvas = None; + # }}} + # # __init__(self, parent, appSize=(840, 630), defaultCanvasPos=(0, 75), defaultCanvasSize=(100, 30), defaultCellSize=(7, 14)): initialisation method def __init__(self, parent, appSize=(840, 630), defaultCanvasPos=(0, 75), defaultCanvasSize=(100, 30), defaultCellSize=(7, 14)): From 1a503979d1a1be684015971b0e64dacc1406abc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Fri, 26 Jan 2018 22:38:28 +0100 Subject: [PATCH 137/148] IrcMiRCARTBot.py:_uploadToImgur(): prevent file object leak via `with' barrier. MiRCARTCanvasExportStore.py:_exportFileToImgur(): prevent file object leak via `with' barrier. --- IrcMiRCARTBot.py | 3 ++- MiRCARTCanvasExportStore.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/IrcMiRCARTBot.py b/IrcMiRCARTBot.py index b028c43..304cd97 100755 --- a/IrcMiRCARTBot.py +++ b/IrcMiRCARTBot.py @@ -180,7 +180,8 @@ class IrcMiRCARTBot(IrcClient.IrcClient): # }}} # {{{ _uploadToImgur(self, imgFilePath, imgName, imgTitle, apiKey): Upload single file to Imgur def _uploadToImgur(self, imgFilePath, imgName, imgTitle, apiKey): - requestImageData = open(imgFilePath, "rb").read() + with open(imgFilePath, "rb") as requestImage: + requestImageData = requestImage.read() requestData = { \ "image": base64.b64encode(requestImageData), \ "key": apiKey, \ diff --git a/MiRCARTCanvasExportStore.py b/MiRCARTCanvasExportStore.py index e7c520d..f8e7035 100644 --- a/MiRCARTCanvasExportStore.py +++ b/MiRCARTCanvasExportStore.py @@ -42,7 +42,8 @@ class MiRCARTCanvasExportStore(): # {{{ _exportFileToImgur(self, apiKey, imgName, imgTitle, pathName): upload single PNG file to Imgur def _exportFileToImgur(self, apiKey, imgName, imgTitle, pathName): - requestImageData = open(pathName, "rb").read() + with open(pathName, "rb") as requestImage: + requestImageData = requestImage.read() requestData = { \ "image": base64.b64encode(requestImageData), \ "key": apiKey, \ From 6d5e081dfbf3f7b8a1ff3a9d79ab44258f6be605 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Tue, 30 Jan 2018 11:17:35 +0100 Subject: [PATCH 138/148] Reduce memory usage by folding nested patch {coordinate,colour} list(s). --- MiRCARTCanvas.py | 20 +++++++++---------- MiRCARTCanvasBackend.py | 38 ++++++++++++++++++------------------- MiRCARTCanvasImportStore.py | 6 +++--- MiRCARTToolCircle.py | 6 +++--- MiRCARTToolFill.py | 8 ++++---- MiRCARTToolLine.py | 8 ++++---- MiRCARTToolRect.py | 2 +- MiRCARTToolSelect.py | 10 +++++----- MiRCARTToolSelectClone.py | 2 +- MiRCARTToolSelectMove.py | 6 +++--- MiRCARTToolText.py | 4 ++-- 11 files changed, 55 insertions(+), 55 deletions(-) diff --git a/MiRCARTCanvas.py b/MiRCARTCanvas.py index b75af72..a66cc43 100644 --- a/MiRCARTCanvas.py +++ b/MiRCARTCanvas.py @@ -41,7 +41,7 @@ class MiRCARTCanvas(wx.Panel): # {{{ _commitPatch(self, patch): XXX def _commitPatch(self, patch): - self.canvasMap[patch[0][1]][patch[0][0]] = patch[1:] + self.canvasMap[patch[1]][patch[0]] = patch[2:] # }}} # {{{ _dispatchDeltaPatches(self, deltaPatches): XXX def _dispatchDeltaPatches(self, deltaPatches): @@ -58,8 +58,8 @@ class MiRCARTCanvas(wx.Panel): self.canvasJournal, eventDc) self._canvasDirtyCursor = True if self.canvasBackend.drawPatch(eventDc, patch): - patchDeltaCell = self.canvasMap[patch[0][1]][patch[0][0]] - patchDelta = [list(patch[0]), *patchDeltaCell.copy()] + patchDeltaCell = self.canvasMap[patch[1]][patch[0]] + patchDelta = [*patch[0:2], *patchDeltaCell] if isCursor: self.canvasJournal.pushCursor(patchDelta) else: @@ -127,10 +127,10 @@ class MiRCARTCanvas(wx.Panel): and numRow < len(newCanvas) \ and numCol < len(newCanvas[numRow]): self._commitPatch([ \ - [numCol, numRow], *newCanvas[numRow][numCol]]) + numCol, numRow, *newCanvas[numRow][numCol]]) self.canvasBackend.drawPatch(eventDc, \ - ([numCol, numRow], \ - *self.canvasMap[numRow][numCol])) + [numCol, numRow, \ + *self.canvasMap[numRow][numCol]]) wx.SafeYield() # }}} # {{{ resize(self, newCanvasSize): XXX @@ -160,20 +160,20 @@ class MiRCARTCanvas(wx.Panel): else: for numRow in range(oldCanvasSize[1]): self.canvasMap[numRow].extend( \ - [[[1, 1], 0, " "]] * deltaCanvasSize[0]) + [[1, 1, 0, " "]] * deltaCanvasSize[0]) for numNewCol in range(oldCanvasSize[0], newCanvasSize[0]): self.canvasBackend.drawPatch( \ - eventDc, [[numNewCol, numRow], \ + eventDc, [numNewCol, numRow, \ *self.canvasMap[numRow][-1]]) if deltaCanvasSize[1] < 0: del self.canvasMap[-1:(deltaCanvasSize[1]-1):-1] else: for numNewRow in range(oldCanvasSize[1], newCanvasSize[1]): self.canvasMap.extend( \ - [[[[1, 1], 0, " "]] * newCanvasSize[0]]) + [[[1, 1, 0, " "]] * newCanvasSize[0]]) for numNewCol in range(newCanvasSize[0]): self.canvasBackend.drawPatch( \ - eventDc, [[numNewCol, numNewRow], \ + eventDc, [numNewCol, numNewRow, \ *self.canvasMap[-1][-1]]) self.canvasSize = newCanvasSize diff --git a/MiRCARTCanvasBackend.py b/MiRCARTCanvasBackend.py index f7b20e2..9cf918f 100644 --- a/MiRCARTCanvasBackend.py +++ b/MiRCARTCanvasBackend.py @@ -33,27 +33,27 @@ class MiRCARTCanvasBackend(): # {{{ _drawBrushPatch(self, eventDc, patch): XXX def _drawBrushPatch(self, eventDc, patch): - absPoint = self._xlatePoint(patch[0]) - brushFg = self._brushes[patch[1][1]] - brushBg = self._brushes[patch[1][0]] - pen = self._pens[patch[1][1]] + absPoint = self._xlatePoint(patch) + brushFg = self._brushes[patch[3]] + brushBg = self._brushes[patch[2]] + pen = self._pens[patch[3]] self._setBrushDc(brushBg, brushFg, eventDc, pen) eventDc.DrawRectangle(*absPoint, *self.cellSize) # }}} # {{{ _drawCharPatch(self, eventDc, patch): XXX def _drawCharPatch(self, eventDc, patch): - absPoint = self._xlatePoint(patch[0]) - brushFg = self._brushes[patch[1][0]] - brushBg = self._brushes[patch[1][1]] - pen = self._pens[patch[1][1]] + absPoint = self._xlatePoint(patch) + brushFg = self._brushes[patch[2]] + brushBg = self._brushes[patch[3]] + pen = self._pens[patch[3]] fontBitmap = wx.Bitmap(*self.cellSize) fontDc = wx.MemoryDC(); fontDc.SelectObject(fontBitmap); - fontDc.SetTextForeground(wx.Colour(MiRCARTColours[patch[1][0]][0:4])) - fontDc.SetTextBackground(wx.Colour(MiRCARTColours[patch[1][1]][0:4])) + fontDc.SetTextForeground(wx.Colour(MiRCARTColours[patch[2]][0:4])) + fontDc.SetTextBackground(wx.Colour(MiRCARTColours[patch[3]][0:4])) fontDc.SetBrush(brushBg); fontDc.SetBackground(brushBg); fontDc.SetPen(pen); fontDc.SetFont(self._font) fontDc.DrawRectangle(0, 0, *self.cellSize) - fontDc.DrawText(patch[3], 0, 0) + fontDc.DrawText(patch[5], 0, 0) eventDc.Blit(*absPoint, *self.cellSize, fontDc, 0, 0) # }}} # {{{ _finiBrushesAndPens(self): XXX @@ -89,18 +89,18 @@ class MiRCARTCanvasBackend(): dc.SetPen(pen) self._lastPen = pen # }}} - # {{{ _xlatePoint(self, relMapPoint): XXX - def _xlatePoint(self, relMapPoint): - return [a*b for a,b in zip(relMapPoint, self.cellSize)] + # {{{ _xlatePoint(self, patch): XXX + def _xlatePoint(self, patch): + return [a*b for a,b in zip(patch[0:2], self.cellSize)] # }}} # {{{ drawPatch(self, eventDc, patch): XXX def drawPatch(self, eventDc, patch): - if patch[0][0] < self.canvasSize[0] \ - and patch[0][0] >= 0 \ - and patch[0][1] < self.canvasSize[1] \ - and patch[0][1] >= 0: - if patch[3] == " ": + if patch[0] < self.canvasSize[0] \ + and patch[0] >= 0 \ + and patch[1] < self.canvasSize[1] \ + and patch[1] >= 0: + if patch[5] == " ": self._drawBrushPatch(eventDc, patch) else: self._drawCharPatch(eventDc, patch) diff --git a/MiRCARTCanvasImportStore.py b/MiRCARTCanvasImportStore.py index e35b4a9..2714e0f 100644 --- a/MiRCARTCanvasImportStore.py +++ b/MiRCARTCanvasImportStore.py @@ -105,7 +105,7 @@ class MiRCARTCanvasImportStore(): inCellState, self._CellState.CS_UNDERLINE) else: inRowCols += 1 - outMap[inCurRow].append((inCurColours, inCellState, inChar)) + outMap[inCurRow].append([*inCurColours, inCellState, inChar]) elif inParseState == self._ParseState.PS_COLOUR_DIGIT0 \ or inParseState == self._ParseState.PS_COLOUR_DIGIT1: if inChar == "," \ @@ -146,8 +146,8 @@ class MiRCARTCanvasImportStore(): # }}} # {{{ importNew(self, newCanvasSize=None): XXX def importNew(self, newCanvasSize=None): - newMap = [[[[1, 1], 0, " "] \ - for x in range(newCanvasSize[0])] \ + newMap = [[[1, 1, 0, " "] \ + for x in range(newCanvasSize[0])] \ for y in range(newCanvasSize[1])] self.parentCanvas.onStoreUpdate(newCanvasSize, newMap) # }}} diff --git a/MiRCARTToolCircle.py b/MiRCARTToolCircle.py index 0fc5fb7..9249b69 100644 --- a/MiRCARTToolCircle.py +++ b/MiRCARTToolCircle.py @@ -45,9 +45,9 @@ class MiRCARTToolCircle(MiRCARTTool): for brushX in range(-radius, radius + 1): if ((brushX**2)+(brushY**2) < (((radius**2)+radius)*0.8)): patch = [ \ - [atPoint[0] + int(originPoint[0]+brushX), \ - atPoint[1] + int(originPoint[1]+brushY)], \ - brushColours, 0, " "] + atPoint[0] + int(originPoint[0]+brushX), \ + atPoint[1] + int(originPoint[1]+brushY), \ + *brushColours, 0, " "] if isLeftDown or isRightDown: dispatchFn(eventDc, False, patch); dispatchFn(eventDc, True, patch); else: diff --git a/MiRCARTToolFill.py b/MiRCARTToolFill.py index c1adc8c..00675e0 100644 --- a/MiRCARTToolFill.py +++ b/MiRCARTToolFill.py @@ -32,17 +32,17 @@ class MiRCARTToolFill(MiRCARTTool): # onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown, dispatchFn, eventDc): XXX def onMouseEvent(self, event, atPoint, brushColours, brushSize, isDragging, isLeftDown, isRightDown, dispatchFn, eventDc): pointStack = [list(atPoint)]; pointsDone = []; - testColour = self.parentCanvas.canvasMap[atPoint[1]][atPoint[0]][0][1] + testColour = self.parentCanvas.canvasMap[atPoint[1]][atPoint[0]][0:2] if isLeftDown or isRightDown: if isRightDown: brushColours = [brushColours[1], brushColours[0]] while len(pointStack) > 0: point = pointStack.pop() pointCell = self.parentCanvas.canvasMap[point[1]][point[0]] - if pointCell[0][1] == testColour: + if pointCell[0:2] == testColour: if not point in pointsDone: - dispatchFn(eventDc, False, [point.copy(), \ - [brushColours[0], brushColours[0]], 0, " "]) + dispatchFn(eventDc, False, [*point, \ + brushColours[0], brushColours[0], 0, " "]) if point[0] > 0: pointStack.append([point[0] - 1, point[1]]) if point[0] < (self.parentCanvas.canvasSize[0] - 1): diff --git a/MiRCARTToolLine.py b/MiRCARTToolLine.py index fe14df7..d70143e 100644 --- a/MiRCARTToolLine.py +++ b/MiRCARTToolLine.py @@ -55,10 +55,10 @@ class MiRCARTToolLine(MiRCARTTool): lineD = 2 * pointDelta[1] - pointDelta[0]; lineY = 0; for lineX in range(pointDelta[0] + 1): for brushStep in range(brushSize[0]): - patch = [[ \ + patch = [ \ originPoint[0] + lineX*lineXX + lineY*lineYX + brushStep, \ - originPoint[1] + lineX*lineXY + lineY*lineYY], \ - brushColours, 0, " "] + originPoint[1] + lineX*lineXY + lineY*lineYY, \ + *brushColours, 0, " "] if isCursor: dispatchFn(eventDc, False, patch); dispatchFn(eventDc, True, patch); else: @@ -83,7 +83,7 @@ class MiRCARTToolLine(MiRCARTTool): self.toolColours = brushColours self.toolOriginPoint = list(atPoint) self.toolState = self.TS_ORIGIN - dispatchFn(eventDc, True, [atPoint, brushColours, 0, " "]) + dispatchFn(eventDc, True, [*atPoint, *brushColours, 0, " "]) elif self.toolState == self.TS_ORIGIN: targetPoint = list(atPoint) originPoint = self.toolOriginPoint diff --git a/MiRCARTToolRect.py b/MiRCARTToolRect.py index d4250ba..66c0094 100644 --- a/MiRCARTToolRect.py +++ b/MiRCARTToolRect.py @@ -43,7 +43,7 @@ class MiRCARTToolRect(MiRCARTTool): brushSize[0] *= 2 for brushRow in range(brushSize[1]): for brushCol in range(brushSize[0]): - patch = [[atPoint[0] + brushCol, atPoint[1] + brushRow],brushColours, 0, " "] + patch = [atPoint[0] + brushCol, atPoint[1] + brushRow, *brushColours, 0, " "] if isLeftDown or isRightDown: dispatchFn(eventDc, False, patch); dispatchFn(eventDc, True, patch); else: diff --git a/MiRCARTToolSelect.py b/MiRCARTToolSelect.py index 68f5539..3084776 100644 --- a/MiRCARTToolSelect.py +++ b/MiRCARTToolSelect.py @@ -53,18 +53,18 @@ class MiRCARTToolSelect(MiRCARTTool): else: curColours = [0, 0] dispatchFn(eventDc, True, \ - [[rectX, rectFrame[0][1]], curColours, 0, " "]) + [rectX, rectFrame[0][1], *curColours, 0, " "]) dispatchFn(eventDc, True, \ - [[rectX, rectFrame[1][1]], curColours, 0, " "]) + [rectX, rectFrame[1][1], *curColours, 0, " "]) for rectY in range(rectFrame[0][1], rectFrame[1][1]+1): if curColours == [0, 0]: curColours = [1, 1] else: curColours = [0, 0] dispatchFn(eventDc, True, \ - [[rectFrame[0][0], rectY], curColours, 0, " "]) + [rectFrame[0][0], rectY, *curColours, 0, " "]) dispatchFn(eventDc, True, \ - [[rectFrame[1][0], rectY], curColours, 0, " "]) + [rectFrame[1][0], rectY, *curColours, 0, " "]) # }}} # @@ -82,7 +82,7 @@ class MiRCARTToolSelect(MiRCARTTool): self.toolState = self.TS_ORIGIN else: dispatchFn(eventDc, True, \ - [list(atPoint), brushColours.copy(), 0, " "]) + [*atPoint, *brushColours, 0, " "]) elif self.toolState == self.TS_ORIGIN: self.toolRect[1] = list(atPoint) if isLeftDown or isRightDown: diff --git a/MiRCARTToolSelectClone.py b/MiRCARTToolSelectClone.py index 14c6024..3d2a5d5 100644 --- a/MiRCARTToolSelectClone.py +++ b/MiRCARTToolSelectClone.py @@ -53,7 +53,7 @@ class MiRCARTToolSelectClone(MiRCARTToolSelect): cellOld = self.toolSelectMap[numRow][numCol] rectY = selectRect[0][1] + numRow rectX = selectRect[0][0] + numCol - dispatchFn(eventDc, isCursor, [[rectX+disp[0], rectY+disp[1]], *cellOld]) + dispatchFn(eventDc, isCursor, [rectX+disp[0], rectY+disp[1], *cellOld]) self._drawSelectRect(newToolRect, dispatchFn, eventDc) self.toolRect = newToolRect diff --git a/MiRCARTToolSelectMove.py b/MiRCARTToolSelectMove.py index 7412062..134826c 100644 --- a/MiRCARTToolSelectMove.py +++ b/MiRCARTToolSelectMove.py @@ -50,14 +50,14 @@ class MiRCARTToolSelectMove(MiRCARTToolSelect): isCursor = True for numRow in range(len(self.toolSelectMap)): for numCol in range(len(self.toolSelectMap[numRow])): - dispatchFn(eventDc, isCursor, [[self.srcRect[0] + numCol, \ - self.srcRect[1] + numRow], [1, 1], 0, " "]) + dispatchFn(eventDc, isCursor, [self.srcRect[0] + numCol, \ + self.srcRect[1] + numRow, 1, 1, 0, " "]) for numRow in range(len(self.toolSelectMap)): for numCol in range(len(self.toolSelectMap[numRow])): cellOld = self.toolSelectMap[numRow][numCol] rectY = selectRect[0][1] + numRow rectX = selectRect[0][0] + numCol - dispatchFn(eventDc, isCursor, [[rectX+disp[0], rectY+disp[1]], *cellOld]) + dispatchFn(eventDc, isCursor, [rectX+disp[0], rectY+disp[1], *cellOld]) self._drawSelectRect(newToolRect, dispatchFn, eventDc) self.toolRect = newToolRect diff --git a/MiRCARTToolText.py b/MiRCARTToolText.py index a3eb686..8026eba 100644 --- a/MiRCARTToolText.py +++ b/MiRCARTToolText.py @@ -42,7 +42,7 @@ class MiRCARTToolText(MiRCARTTool): self.textColours = brushColours.copy() if self.textPos == None: self.textPos = list(atPoint) - dispatchFn(eventDc, False, [self.textPos, self.textColours, 0, keyChar]) + dispatchFn(eventDc, False, [*self.textPos, *self.textColours, 0, keyChar]) if self.textPos[0] < (self.parentCanvas.canvasSize[0] - 1): self.textPos[0] += 1 elif self.textPos[1] < (self.parentCanvas.canvasSize[1] - 1): @@ -65,6 +65,6 @@ class MiRCARTToolText(MiRCARTTool): if self.textColours == None: self.textColours = brushColours.copy() self.textPos = list(atPoint) - dispatchFn(eventDc, True, [self.textPos, self.textColours, 0, "_"]) + dispatchFn(eventDc, True, [*self.textPos, *self.textColours, 0, "_"]) # vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 From 8e91d1269e6b6a64edf46896c3701afc03a03919 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Tue, 30 Jan 2018 20:34:02 +0100 Subject: [PATCH 139/148] {IrcMiRCARTBot,MiRCARTToPngFile}.py: reduce memory usage by folding nested patch {coordinate,colour} list(s). Followup to . --- IrcMiRCARTBot.py | 10 +++++----- MiRCARTToPngFile.py | 34 +++++++++++++++++----------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/IrcMiRCARTBot.py b/IrcMiRCARTBot.py index 304cd97..f4499ff 100755 --- a/IrcMiRCARTBot.py +++ b/IrcMiRCARTBot.py @@ -148,11 +148,11 @@ class IrcMiRCARTBot(IrcClient.IrcClient): for numRow in range(len(canvasStore.outMap)): if len(canvasStore.outMap[numRow]) != numRowCols: for numColOff in range(numRowCols - len(canvasStore.outMap[numRow])): - canvasStore.outMap[numRow].append([[1, 1], 0, " "]) - canvasStore.outMap[numRow].insert(0, [[1, 1], 0, " "]) - canvasStore.outMap[numRow].append([[1, 1], 0, " "]) - canvasStore.outMap.insert(0, [[[1, 1], 0, " "]] * len(canvasStore.outMap[0])) - canvasStore.outMap.append([[[1, 1], 0, " "]] * len(canvasStore.outMap[0])) + canvasStore.outMap[numRow].append([1, 1, 0, " "]) + canvasStore.outMap[numRow].insert(0, [1, 1, 0, " "]) + canvasStore.outMap[numRow].append([1, 1, 0, " "]) + canvasStore.outMap.insert(0, [[1, 1, 0, " "]] * len(canvasStore.outMap[0])) + canvasStore.outMap.append([[1, 1, 0, " "]] * len(canvasStore.outMap[0])) MiRCARTToPngFile(canvasStore.outMap, "DejaVuSansMono.ttf", 11).export(imgTmpFilePath) imgurResponse = self._uploadToImgur(imgTmpFilePath, "MiRCART image", "MiRCART image", "c9a6efb3d7932fd") if imgurResponse[0] == 200: diff --git a/MiRCARTToPngFile.py b/MiRCARTToPngFile.py index 16661b8..922fb4f 100755 --- a/MiRCARTToPngFile.py +++ b/MiRCARTToPngFile.py @@ -89,35 +89,35 @@ class MiRCARTToPngFile: for inCurCol in range(len(self.inCanvasMap[inCurRow])): inCurCell = self.inCanvasMap[inCurRow][inCurCol] outColours = [0, 0] - if inCurCell[1] & MiRCARTCanvasImportStore.MiRCARTCanvasImportStore._CellState.CS_BOLD: - if inCurCell[2] != " ": - if inCurCell[2] == "█": - outColours[1] = self._ColourMapNormal[inCurCell[0][0]] + if inCurCell[2] & MiRCARTCanvasImportStore.MiRCARTCanvasImportStore._CellState.CS_BOLD: + if inCurCell[3] != " ": + if inCurCell[3] == "█": + outColours[1] = self._ColourMapNormal[inCurCell[0]] else: - outColours[0] = self._ColourMapBold[inCurCell[0][0]] - outColours[1] = self._ColourMapNormal[inCurCell[0][1]] + outColours[0] = self._ColourMapBold[inCurCell[0]] + outColours[1] = self._ColourMapNormal[inCurCell[1]] else: - outColours[1] = self._ColourMapNormal[inCurCell[0][1]] + outColours[1] = self._ColourMapNormal[inCurCell[1]] else: - if inCurCell[2] != " ": - if inCurCell[2] == "█": - outColours[1] = self._ColourMapNormal[inCurCell[0][0]] + if inCurCell[3] != " ": + if inCurCell[3] == "█": + outColours[1] = self._ColourMapNormal[inCurCell[0]] else: - outColours[0] = self._ColourMapNormal[inCurCell[0][0]] - outColours[1] = self._ColourMapNormal[inCurCell[0][1]] + outColours[0] = self._ColourMapNormal[inCurCell[0]] + outColours[1] = self._ColourMapNormal[inCurCell[1]] else: - outColours[1] = self._ColourMapNormal[inCurCell[0][1]] + outColours[1] = self._ColourMapNormal[inCurCell[1]] outImgDraw.rectangle((*outCurPos, \ outCurPos[0] + self.outImgFontSize[0], \ outCurPos[1] + self.outImgFontSize[1]), \ fill=(*outColours[1], 255)) - if not inCurCell[2] in " █" \ + if not inCurCell[3] in " █" \ and outColours[0] != outColours[1]: # XXX implement italic outImgDraw.text(outCurPos, \ - inCurCell[2], (*outColours[0], 255), self.outImgFont) - if inCurCell[1] & MiRCARTCanvasImportStore.MiRCARTCanvasImportStore._CellState.CS_UNDERLINE: - outColours[0] = self._ColourMapNormal[inCurCell[0][0]] + inCurCell[3], (*outColours[0], 255), self.outImgFont) + if inCurCell[2] & MiRCARTCanvasImportStore.MiRCARTCanvasImportStore._CellState.CS_UNDERLINE: + outColours[0] = self._ColourMapNormal[inCurCell[0]] self._drawUnderLine(outCurPos, \ self.outImgFontSize, \ outImgDraw, (*outColours[0], 255)) From 3b70d42784be76f9b3c71318c430a7aeb2a573ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Tue, 1 May 2018 23:30:36 +0200 Subject: [PATCH 140/148] MiRCARTCanonicalise.py: canonicalise mIRC art {from,to} file (for munki.) --- MiRCARTCanonicalise.py | 60 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100755 MiRCARTCanonicalise.py diff --git a/MiRCARTCanonicalise.py b/MiRCARTCanonicalise.py new file mode 100755 index 0000000..0e8a647 --- /dev/null +++ b/MiRCARTCanonicalise.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +# +# MiRCARTCanonicalise.py -- canonicalise mIRC art {from,to} file (for munki) +# Copyright (c) 2018 Lucio Andrés Illanes Albornoz +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +from MiRCARTCanvasImportStore import MiRCARTCanvasImportStore +import sys + +def canonicalise(inPathName): + canvasStore = MiRCARTCanvasImportStore(inPathName) + inMap = canvasStore.outMap.copy(); del canvasStore; + with open(inPathName, "w+") as outFile: + lastAttribs = MiRCARTCanvasImportStore._CellState.CS_NONE + lastColours = None + for inCurRow in range(len(inMap)): + for inCurCol in range(len(inMap[inCurRow])): + inCurCell = inMap[inCurRow][inCurCol] + if lastAttribs != inCurCell[2]: + if inCurCell[2] & MiRCARTCanvasImportStore._CellState.CS_BOLD: + print("\u0002", end="", file=outFile) + if inCurCell[2] & MiRCARTCanvasImportStore._CellState.CS_UNDERLINE: + print("\u001f", end="", file=outFile) + lastAttribs = inCurCell[2] + if lastColours == None or lastColours != inCurCell[:2]: + print("\u0003{:02d},{:02d}{}".format(*inCurCell[:2], inCurCell[3]), end="", file=outFile) + lastColours = inCurCell[:2] + else: + print(inCurCell[3], end="", file=outFile) + print("\n", end="", file=outFile) + +# +# Entry point +def main(*argv): + canonicalise(argv[1]) +if __name__ == "__main__": + if (len(sys.argv) - 1) != 1: + print("usage: {} ".format(sys.argv[0]), file=sys.stderr) + else: + main(*sys.argv) + +# vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 From 99c0c149936e5c14d64b016e6c6b231c693b72a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Wed, 2 May 2018 17:08:45 +0200 Subject: [PATCH 141/148] MiRCARTCheckLineLengths.sh: check mIRC art line lengths. --- MiRCARTCheckLineLengths.sh | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100755 MiRCARTCheckLineLengths.sh diff --git a/MiRCARTCheckLineLengths.sh b/MiRCARTCheckLineLengths.sh new file mode 100755 index 0000000..17702cf --- /dev/null +++ b/MiRCARTCheckLineLengths.sh @@ -0,0 +1,33 @@ +#!/bin/sh +# +# MiRCARTCheckLineLengths.py -- check mIRC art line lengths +# Copyright (c) 2018 Lucio Andrés Illanes Albornoz +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +for FNAME in "${@}"; do + FNAME_LINES="$(wc -l "${FNAME}" | awk '{print $1}')"; + for FNAME_LINE in $(seq "${FNAME_LINES}"); do + printf "%-5d %-5d %s\n" \ + "$(sed -n "${FNAME_LINE}p" "${FNAME}" | wc -c)" \ + "${FNAME_LINE}" "${FNAME}"; + done; +done | sort -nk1; + +# vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 From 95f7dd113c5bbb3cdffb3c715e4cd84f955fb122 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Thu, 24 May 2018 08:52:20 +0200 Subject: [PATCH 142/148] IrcMiRCARTBot.py, MiRCARTCanvasInterface.py: remove Imgur API key from repository (via aaa, alghazi, amomp, anji, arab, astra, astra`, biobag, biobag__, bj0rn, blomp, boomp, bromp, brr, butts, buttvomit, c003y, Caku, chomp, chrono_, cooey, CosbyX, darkmage_, dboard, dehuman, dOm3r, eddb, efukt, ep^, er, era, eraser, erratic, erratic_, gnomp, gromp, interdom3, JEWS, kobach, l1tup, Lions, lul, lulz, lulzee, lulzy, Matthew, MercyX, moomp, mr_vile, muff, munki, n0v, nk9k, ooomp, OVH, pcap, pinchy, plop, pngbot, poccri, poomp, promp, pump-, pumpbull, pyrex, rain, rObOtNiK, rondito, scd, SEEEEN, Shapes, snadge, spidy, spinsane, spoke, spomp, stomp, TACO, tetedupet, toohighto, tromp, twomp, vap0r, vapor, venus, virtuald, vixen, whomp, wreathman, wromp, yoomp, z0z0, zen_, and zoomp.) --- .gitignore | 1 + IrcMiRCARTBot.py | 4 +++- MiRCARTCanvas.py | 2 ++ MiRCARTCanvasInterface.py | 5 +++-- MiRCARTImgurApiKey.py.template | 28 ++++++++++++++++++++++++++++ 5 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 MiRCARTImgurApiKey.py.template diff --git a/.gitignore b/.gitignore index 7a00c01..04ad9ab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ +MiRCARTImgurApiKey.py __pycache__/ *.sw[op] diff --git a/IrcMiRCARTBot.py b/IrcMiRCARTBot.py index f4499ff..170d3ee 100755 --- a/IrcMiRCARTBot.py +++ b/IrcMiRCARTBot.py @@ -28,12 +28,14 @@ import json import IrcClient import requests, urllib.request from MiRCARTCanvasImportStore import MiRCARTCanvasImportStore +from MiRCARTImgurApiKey import MiRCARTImgurApiKey from MiRCARTToPngFile import MiRCARTToPngFile class IrcMiRCARTBot(IrcClient.IrcClient): """IRC<->MiRC2png bot""" clientChannelLastMessage = clientChannelOps = clientChannel = None clientChannelRejoin = None + imgurApiKey = MiRCARTImgurApiKey.imgurApiKey # {{{ ContentTooLargeException(Exception): Raised by _urlretrieveReportHook() given download size > 1 MB class ContentTooLargeException(Exception): @@ -154,7 +156,7 @@ class IrcMiRCARTBot(IrcClient.IrcClient): canvasStore.outMap.insert(0, [[1, 1, 0, " "]] * len(canvasStore.outMap[0])) canvasStore.outMap.append([[1, 1, 0, " "]] * len(canvasStore.outMap[0])) MiRCARTToPngFile(canvasStore.outMap, "DejaVuSansMono.ttf", 11).export(imgTmpFilePath) - imgurResponse = self._uploadToImgur(imgTmpFilePath, "MiRCART image", "MiRCART image", "c9a6efb3d7932fd") + imgurResponse = self._uploadToImgur(imgTmpFilePath, "MiRCART image", "MiRCART image", self.imgurApiKey) if imgurResponse[0] == 200: self._log("Uploaded as: {}".format(imgurResponse[1])) self.queue("PRIVMSG", message[2], "8/!\\ Uploaded as: {}".format(imgurResponse[1])) diff --git a/MiRCARTCanvas.py b/MiRCARTCanvas.py index a66cc43..8c9c2c5 100644 --- a/MiRCARTCanvas.py +++ b/MiRCARTCanvas.py @@ -27,6 +27,7 @@ from MiRCARTCanvasJournal import MiRCARTCanvasJournal from MiRCARTCanvasExportStore import MiRCARTCanvasExportStore, haveMiRCARTToPngFile, haveUrllib from MiRCARTCanvasImportStore import MiRCARTCanvasImportStore from MiRCARTCanvasInterface import MiRCARTCanvasInterface +from MiRCARTImgurApiKey import MiRCARTImgurApiKey import wx class MiRCARTCanvas(wx.Panel): @@ -38,6 +39,7 @@ class MiRCARTCanvas(wx.Panel): canvasBackend = canvasJournal = None canvasExportStore = canvasImportStore = None canvasInterface = None + imgurApiKey = MiRCARTImgurApiKey.imgurApiKey # {{{ _commitPatch(self, patch): XXX def _commitPatch(self, patch): diff --git a/MiRCARTCanvasInterface.py b/MiRCARTCanvasInterface.py index c0543be..1f08baa 100644 --- a/MiRCARTCanvasInterface.py +++ b/MiRCARTCanvasInterface.py @@ -34,6 +34,7 @@ import os, wx class MiRCARTCanvasInterface(): """XXX""" + imgurApiKey = None parentCanvas = parentFrame = canvasPathName = canvasTool = None # {{{ _dialogSaveChanges(self) @@ -136,8 +137,8 @@ class MiRCARTCanvasInterface(): # {{{ canvasExportImgur(self, event): XXX def canvasExportImgur(self, event): self.parentCanvas.SetCursor(wx.Cursor(wx.CURSOR_WAIT)) - imgurResult = self.parentCanvas.canvasExportStore.exportBitmapToImgur( \ - "c9a6efb3d7932fd", self.parentCanvas.canvasBackend.canvasBitmap, \ + imgurResult = self.parentCanvas.canvasExportStore.exportBitmapToImgur( \ + self.imgurApiKey, self.parentCanvas.canvasBackend.canvasBitmap, \ "", "", wx.BITMAP_TYPE_PNG) self.parentCanvas.SetCursor(wx.Cursor(wx.NullCursor)) if imgurResult[0] == 200: diff --git a/MiRCARTImgurApiKey.py.template b/MiRCARTImgurApiKey.py.template new file mode 100644 index 0000000..6580ac7 --- /dev/null +++ b/MiRCARTImgurApiKey.py.template @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# +# MiRCARTImgurApiKey.py -- melp? +# Copyright (c) 2018 Lucio Andrés Illanes Albornoz +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +class MiRCARTImgurApiKey(object): + imgurApiKey = None + +# vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 From 9a9d5d858dc1655ce3da8d2e8cd68b6d8fb1b2f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Thu, 24 May 2018 08:59:00 +0200 Subject: [PATCH 143/148] IrcMiRCARTBot.py:_dispatchPrivmsg(): lower rate limit to once every 5 seconds (via aaa, alghazi, amomp, anji, arab, astra, astra`, biobag, biobag__, bj0rn, blomp, boomp, bromp, brr, butts, buttvomit, c003y, Caku, chomp, chrono_, cooey, CosbyX, darkmage_, dboard, deh uman, dOm3r, eddb, efukt, ep^, er, era, eraser, erratic, erratic_, gnomp, gromp, interdom3, JEWS, kobach, l1tup, Lions, lul, lulz, lulzee, lulzy, Matthew, MercyX, moomp, mr_vile, muff, munki, n0v, nk9k, ooomp, OVH, pcap, pinchy, plop, pngbot, poccri, poomp, promp, pump-, pu mpbull, pyrex, rain, rObOtNiK, rondito, scd, SEEEEN, Shapes, snadge, spidy, spinsane, spoke, spomp, stomp, TACO, tetedupet, toohighto, tromp, twomp, vap0r, vapor, venus, virtuald, vixen, whomp, wreathman, wromp, yoomp, z0z0, zen_, and zoomp.) --- IrcMiRCARTBot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IrcMiRCARTBot.py b/IrcMiRCARTBot.py index 170d3ee..4244c89 100755 --- a/IrcMiRCARTBot.py +++ b/IrcMiRCARTBot.py @@ -110,7 +110,7 @@ class IrcMiRCARTBot(IrcClient.IrcClient): def _dispatchPrivmsg(self, message): if message[2].lower() == self.clientChannel.lower() \ and message[3].startswith("!pngbot "): - if (int(time.time()) - self.clientLastMessage) < 30: + if (int(time.time()) - self.clientLastMessage) < 5: self._log("Ignoring request on {} from {} due to rate limit: {}".format(message[2].lower(), message[0], message[3])) return elif message[0].split("!")[0].lower() not in self.clientChannelOps: From 8d6f6ef56e59a1427e9d2ec7d51263209522fd60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Sun, 17 Jun 2018 09:45:00 +0200 Subject: [PATCH 144/148] MiRCARTCanonicalise.py: correctly reset last{Attribs,Colours} on each new row (fixes issue #1.) --- MiRCARTCanonicalise.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MiRCARTCanonicalise.py b/MiRCARTCanonicalise.py index 0e8a647..afda9b4 100755 --- a/MiRCARTCanonicalise.py +++ b/MiRCARTCanonicalise.py @@ -29,9 +29,9 @@ def canonicalise(inPathName): canvasStore = MiRCARTCanvasImportStore(inPathName) inMap = canvasStore.outMap.copy(); del canvasStore; with open(inPathName, "w+") as outFile: - lastAttribs = MiRCARTCanvasImportStore._CellState.CS_NONE - lastColours = None for inCurRow in range(len(inMap)): + lastAttribs = MiRCARTCanvasImportStore._CellState.CS_NONE + lastColours = None for inCurCol in range(len(inMap[inCurRow])): inCurCell = inMap[inCurRow][inCurCol] if lastAttribs != inCurCell[2]: From 8b483e262e173037543601d42f0cdc772236f82c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Thu, 21 Jun 2018 09:04:22 +0200 Subject: [PATCH 145/148] IrcMiRCARTBot.py:_dispatchPrivmsg(): print message from website if upload fails. IrcMiRCARTBot.py:_uploadToImgur(): additionally return responseHttp.text on failure. --- IrcMiRCARTBot.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/IrcMiRCARTBot.py b/IrcMiRCARTBot.py index 4244c89..9bd7003 100755 --- a/IrcMiRCARTBot.py +++ b/IrcMiRCARTBot.py @@ -163,7 +163,9 @@ class IrcMiRCARTBot(IrcClient.IrcClient): self.clientLastMessage = int(time.time()) else: self._log("Upload failed with HTTP status code {}".format(imgurResponse[0])) + self._log("Message from website: {}".format(imgurResponse[1])) self.queue("PRIVMSG", message[2], "4/!\\ Upload failed with HTTP status code {}!".format(imgurResponse[0])) + self.queue("PRIVMSG", message[2], "4/!\\ Message from website: {}".format(imgurResponse[1])) if os.path.isfile(asciiTmpFilePath): os.remove(asciiTmpFilePath) if os.path.isfile(imgTmpFilePath): @@ -197,7 +199,7 @@ class IrcMiRCARTBot(IrcClient.IrcClient): if responseHttp.status_code == 200: return [200, responseDict.get("data").get("link")] else: - return [responseHttp.status_code] + return [responseHttp.status_code, responseHttp.text] # }}} # {{{ _urlretrieveReportHook(count, blockSize, totalSize): Limit downloads to 1 MB def _urlretrieveReportHook(count, blockSize, totalSize): From 7a1ec135b2c6a77e63efcee060636fbc391cd64c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Fri, 22 Jun 2018 11:38:47 +0200 Subject: [PATCH 146/148] MiRCARTCanvasImportStore.py:importTextFile(): correctly open() pathName w/ encoding="utf-8" (via munki.) --- MiRCARTCanvasImportStore.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiRCARTCanvasImportStore.py b/MiRCARTCanvasImportStore.py index 2714e0f..a070b27 100644 --- a/MiRCARTCanvasImportStore.py +++ b/MiRCARTCanvasImportStore.py @@ -70,7 +70,7 @@ class MiRCARTCanvasImportStore(): # }}} # {{{ importTextFile(self, pathName): XXX def importTextFile(self, pathName): - self.inFile = open(pathName, "r") + self.inFile = open(pathName, "r", encoding="utf-8") self.inSize = self.outMap = None; inCurColourSpec = ""; inCurRow = -1; inLine = self.inFile.readline() From 05d3d28f1d0f35a4ece83093164c59e646f05601 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Tue, 26 Jun 2018 15:39:29 +0200 Subject: [PATCH 147/148] MiRCARTToPngFiles.sh: added. --- MiRCARTToPngFiles.sh | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100755 MiRCARTToPngFiles.sh diff --git a/MiRCARTToPngFiles.sh b/MiRCARTToPngFiles.sh new file mode 100755 index 0000000..53e17b1 --- /dev/null +++ b/MiRCARTToPngFiles.sh @@ -0,0 +1,28 @@ +#!/bin/sh +# +# MiRCARTToPngFiles.sh -- convert ASCII(s) w/ mIRC control codes to monospaced PNG(s) (for EFnet #MiRCART) +# Copyright (c) 2018 Lucio Andrés Illanes Albornoz +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +for FNAME in "${@}"; do + ./MiRCARTToPngFile.py "${FNAME}" "${FNAME%.txt}.png"; +done; + +# vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 From 624ab9ca714be22603c9b52f728113f5d16631b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Thu, 28 Jun 2018 18:18:48 +0200 Subject: [PATCH 148/148] IrcMiRCARTBot.py: adds optional -[46] force IPv[46] flag. --- IrcClient.py | 10 ++++++---- IrcMiRCARTBot.py | 30 +++++++++++++++++++----------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/IrcClient.py b/IrcClient.py index 4891027..ba1d2ec 100644 --- a/IrcClient.py +++ b/IrcClient.py @@ -38,12 +38,14 @@ class IrcClient: self.clientSocket.close() self.clientSocket = self.clientSocketFile = None; # }}} - # {{{ connect(self, timeout=None): Connect to server and register w/ optional timeout - def connect(self, timeout=None): - self.clientSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + # {{{ connect(self, preferFamily=socket.AF_INET, timeout=None): Connect to server and register w/ optional timeout + def connect(self, preferFamily=socket.AF_INET, timeout=None): + gaiInfo = socket.getaddrinfo(self.serverHname, self.serverPort, + preferFamily, socket.SOCK_STREAM, socket.IPPROTO_TCP) + self.clientSocket = socket.socket(*gaiInfo[0][:3]) self.clientSocket.setblocking(0) try: - self.clientSocket.connect((self.serverHname, int(self.serverPort))) + self.clientSocket.connect(gaiInfo[0][4]) except BlockingIOError: pass if timeout: diff --git a/IrcMiRCARTBot.py b/IrcMiRCARTBot.py index 9bd7003..1c1f7e3 100755 --- a/IrcMiRCARTBot.py +++ b/IrcMiRCARTBot.py @@ -22,8 +22,9 @@ # SOFTWARE. # +from getopt import getopt, GetoptError import base64 -import os, sys, time +import os, socket, sys, time import json import IrcClient import requests, urllib.request @@ -206,10 +207,10 @@ class IrcMiRCARTBot(IrcClient.IrcClient): if (totalSize > pow(2,20)): raise IrcMiRCARTBot.ContentTooLargeException # }}} - # {{{ connect(self, timeout=None): Connect to server and (re)initialise w/ optional timeout - def connect(self, timeout=None): + # {{{ connect(self, preferFamily=0, timeout=None): Connect to server and (re)initialise w/ optional timeout + def connect(self, preferFamily=0, timeout=None): self._log("Connecting to {}:{}...".format(self.serverHname, self.serverPort)) - if super().connect(timeout): + if super().connect(preferFamily=preferFamily, timeout=timeout): self._log("Connected to {}:{}.".format(self.serverHname, self.serverPort)) self._log("Registering on {}:{} as {}, {}, {}...".format(self.serverHname, self.serverPort, self.clientNick, self.clientIdent, self.clientGecos)) self.clientLastMessage = 0; self.clientChannelOps = []; @@ -255,18 +256,25 @@ class IrcMiRCARTBot(IrcClient.IrcClient): # # Entry point -def main(*argv): - _IrcMiRCARTBot = IrcMiRCARTBot(*argv[1:]) +def main(optdict, *argv): + _IrcMiRCARTBot = IrcMiRCARTBot(*argv) while True: - if _IrcMiRCARTBot.connect(15): + if "-4" in optdict: + preferFamily = socket.AF_INET + elif "-6" in optdict: + preferFamily = socket.AF_INET6 + else: + preferFamily = 0 + if _IrcMiRCARTBot.connect(preferFamily=preferFamily, timeout=15): _IrcMiRCARTBot.dispatch() _IrcMiRCARTBot.close() time.sleep(15) if __name__ == "__main__": - if ((len(sys.argv) - 1) < 1)\ - or ((len(sys.argv) - 1) > 4): - print("usage: {} " \ + optlist, argv = getopt(sys.argv[1:], "46") + optdict = dict(optlist) + if len(argv) < 1 or len(argv) > 4: + print("usage: {} [-4|-6] " \ " " \ "[] " \ "[] " \ @@ -274,6 +282,6 @@ if __name__ == "__main__": "[] " \ "[] ".format(sys.argv[0]), file=sys.stderr) else: - main(*sys.argv) + main(optdict, *argv) # vim:expandtab foldmethod=marker sw=4 ts=4 tw=120