From 095990f49e90822163d9072093fdf6580afe3f82 Mon Sep 17 00:00:00 2001 From: "D. MacCarthy" Date: Sat, 27 May 2023 08:26:17 -0600 Subject: [PATCH] Release v3.0.3 --- sc8pr/__init__.py | 6 +- sc8pr/gui/scroll.py | 4 +- sc8pr/gui/tbcanvas.py | 235 ++++++++++++++++++++++++++++++++++++++++++ setup.cfg | 2 +- 4 files changed, 241 insertions(+), 6 deletions(-) create mode 100644 sc8pr/gui/tbcanvas.py diff --git a/sc8pr/__init__.py b/sc8pr/__init__.py index 04c86f2..5fa2581 100644 --- a/sc8pr/__init__.py +++ b/sc8pr/__init__.py @@ -16,7 +16,7 @@ # along with "sc8pr". If not, see . -version = 3, 0, "dev3" +version = 3, 0, 3 print("sc8pr {}.{}.{}: https://dmaccarthy.github.io/sc8pr".format(*version)) import sys, struct @@ -1111,8 +1111,8 @@ def find(self, criteria, recursive=False): for gr in (self.everything() if recursive else self): if criteria(gr): yield gr - def scroll(self, dx=0, dy=0): - raise NotImplementedError("Use ScrollCanvas class.") + # def scroll(self, dx=0, dy=0): + # raise NotImplementedError("Use ScrollCanvas class.") def cover(self): return Image(self.size, "#ffffffc0").config(anchor=TOPLEFT) diff --git a/sc8pr/gui/scroll.py b/sc8pr/gui/scroll.py index 684c2d0..a1946a2 100644 --- a/sc8pr/gui/scroll.py +++ b/sc8pr/gui/scroll.py @@ -1,4 +1,4 @@ -# Copyright 2015-2021 D.G. MacCarthy +# Copyright 2015-2023 D.G. MacCarthy # # This file is part of "sc8pr". # @@ -18,7 +18,7 @@ import pygame from sc8pr.gui.slider import Slider, Knob -from sc8pr import Image, Canvas, Sketch, BOTTOMLEFT, TOPRIGHT +from sc8pr import Canvas, Sketch, BOTTOMLEFT, TOPRIGHT CANVAS = 1 SCROLL = 2 diff --git a/sc8pr/gui/tbcanvas.py b/sc8pr/gui/tbcanvas.py new file mode 100644 index 0000000..ecc80b8 --- /dev/null +++ b/sc8pr/gui/tbcanvas.py @@ -0,0 +1,235 @@ +# Copyright 2015-2023 D.G. MacCarthy +# +# This file is part of "sc8pr". +# +# "sc8pr" is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# "sc8pr" is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with "sc8pr". If not, see . + + +"EXPERIMENTAL -- Scrollable canvas for text buttons" + +import os +from fnmatch import fnmatch +from sc8pr import Canvas, TOPLEFT, LEFT, TOPRIGHT +from sc8pr.gui.button import Button, OPTIONS +from sc8pr.gui.slider import Slider +from sc8pr.text import Text, Font +from sc8pr.util import resolvePath + +FOLDERS = 1 +FILES = 2 +SAVE = 3 + + +def fnmatch_any(f, pattern): + "Match any of the patterns" + pattern = pattern.split(";") + f = os.path.split(f)[1] + for p in pattern: + if fnmatch(f, p.strip()): + return True + return False + + +class _Button(Button): + "Customize buttons for TextButtonCanvas" + allowButton = 1, 4, 5 + + @property + def value(self): + "Get the button value" + return getattr(self, "_value", self[0].data) + + @value.setter + def value(self, v): + "Set the button value" + if v is None: + if hasattr(self, "_value"): + delattr(self, "_value") + else: + self._value = v + + def onclick(self, ev): + "Handle button click events" + cv = self.canvas + if ev.button in (4, 5): + slider = list(cv.instOf(_Slider)) + if slider: + slider[0].val += -1 if ev.button == 4 else 1 + slider[0].onchange() + elif ev.button == 1: + if self._status < 4: + if self.selectable: + if cv.uniqueSelect: + for btn in cv.instOf(_Button): + if btn.selected and btn is not self: + btn.selected = False + self.selected = not self.selected + ev.targetButton = self + cv._click(ev) + + +class _Slider(Slider): + "Customize slider for TextButtonCanvas" + + reverseWheel = True + + def onchange(self, ev=None): + "Scroll canvas with slider control" + self.canvas.scrollTo(round(self.val)) + + +class TextButtonCanvas(Canvas): + "Canvas subclass for text buttons" + options = ("#ffffff00", ) + OPTIONS[1:3] + buttonStyle = dict(weight=0) + sliderStyle = dict(bg="#f0f0ff") + knob = None + uniqueSelect = True + _lastClick = None + + def __init__(self, size, bg=None): + "Initialize the instance" + super().__init__(size, bg=bg) + self._h = 0 + + def _count_overflow(self, dy): + "Count the number of buttons that do not fit in the canvas" + btns = list(self.instOf(_Button)) + h = n = 0 + while h < dy: + n += 1 + h += btns[n].height + return n + + def scrollTo(self, n=0): + "Reposition the buttons so button n appears at the top of the canvas" + y = 0 + hide = self.height + 1 + for btn in self.instOf(_Button): + if btn.status < 2: + btn.status = 0 + if n > 0: + btn.config(pos=(0, hide)) + n -= 1 + else: + btn.config(pos=(0, y)) + y += btn.height + + def purge(self, recursive=False): + Canvas.purge(self, recursive) + self._h = 0 + return self + + def text(self, *args, **kwargs): + "Add a text button(s)" + h = self._h + for text in args: + t = Text(text).config(**kwargs) + b = _Button((self.width, t.height), self.options) #.bind(onaction=handle) + b += t.config(anchor=LEFT, pos=(0, t.height/2)) + self += b.config(anchor=TOPLEFT, pos=(0, h), **self.buttonStyle) + h += t.height + self._h = h + if h > self.height: + self.removeItems("ScrollBar") + n = self._count_overflow(h - self.height) + w, h = self.size + slider = _Slider((16, h), self.knob, 0, n, n).config(anchor=TOPRIGHT, pos=(w, 0)) + self["ScrollBar"] = slider.config(**self.sliderStyle) + self.scrollTo() + return self + + def _click(self, ev): + btn = ev.targetButton + sk = self.sketch + f1 = sk.frameCount + dbl = False + if ev.button == 1: + if self._lastClick: + btn0, f0 = self._lastClick + if btn is btn0 and f1 - f0 < sk.frameRate / 2: + dbl = True + if btn.selectable: + btn.selected = True + self._lastClick = btn, f1 + self.bubble("onaction" if dbl else "onclick", ev) + + +class FileListCanvas(TextButtonCanvas): + "File list canvas" + folderStyle = dict(color="blue") + _pattern = "*" + _mode = FOLDERS + FILES + _folder = None + + def __init__(self, size, bg=None, adjustHeight=False, **kwargs): + "Initialize the instance" + self.font = f = dict(font=Font.sans(), fontSize=12, fontStyle=0, padding=2) + f.update(kwargs) + if adjustHeight: + h = Font._get_h(f["font"], f["fontSize"], f["fontStyle"]) + 2 * f["padding"] + size = size[0], h * round(size[1] / h) + self.buttonHeight = h + super().__init__(size, bg=bg) + + @property + def folder(self): + "Return the current folder path" + return self._folder + + def selected(self, mode=2): + "Return a list of selected folders/files" + s = [] + for b in self: + if b.name != "ScrollBar" and b.selected: + f = resolvePath(b.value, self._folder, True) + if mode & FOLDERS and os.path.isdir(f) or mode & FILES and os.path.isfile(f): + s.append(f) + return s + + def openFolder(self): + "Open the selected folder" + s = self.selected(FOLDERS) + if s: + self.showFiles(s[0]) + return True + else: + return False + + def showFiles(self, folder=".", pattern=None, mode=None): + "Create buttons for a set of folders and/or files" + if pattern is None: + pattern = self._pattern + else: + self._pattern = pattern + if mode is None: + mode = self._mode + else: + self._mode = mode + flist = os.listdir(folder) + self._folder = os.path.abspath(folder) + a, b = ["[Parent Folder]"], [] + for f in flist: + full = resolvePath(f, self._folder, True) + if os.path.isdir(full): + a.append(f) + elif fnmatch_any(full, pattern): + b.append(f) + flist = a if mode == FOLDERS else b if mode == FILES else (a+b) + self.purge().text(*flist, **self.font) + if mode & FOLDERS: + self[0].value = ".." + for i in range(len(a)): + self[i][0].config(**self.folderStyle) + return self diff --git a/setup.cfg b/setup.cfg index 831c816..f890f08 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = sc8pr -version = 3.0.dev3 +version = 3.0.3 author = D.G. MacCarthy author_email = sc8pr.py@gmail.com url = https://dmaccarthy.github.io/sc8pr