# Copyright 2020 Saad Mairaj # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import sys if sys.version_info.major == 2: import ttk import Tkinter as _tk from Tkinter import _cnfmerge elif sys.version_info.major == 3: from tkinter import ttk from tkinter import _cnfmerge import tkinter as _tk try: import colour as C except ImportError as e: import os if sys.version_info.major == 2: import tkMessageBox as ms elif sys.version_info.major == 3: import tkinter.messagebox as ms error_win = _tk.Tk() error_win.withdraw() message = """ Module "tkmacosx" is dependent on "colour" Module. Do you want to install it with pip now? Or install it manually later. Link to the colour github: https://github.com/vaab/colour""" if ms._show(e, message, 'warning', 'yesno') == 'yes': error_win.destroy() os.system('pip install colour') import colour as C else: sys.exit(0) import tkmacosx.variables as tkv def delta(evt): """Modified delta to work with all platforms.""" if evt.num == 5 or evt.delta < 0: return -1 return 1 def _agsmerge(args): """Internal functions.\n Merges lists/tuples.""" a = [] if isinstance(args, (tuple, list)): for i in args: if isinstance(i, (tuple, list)): a.extend(i) else: a.append(i) return a or args def check_function_equality(func1, func2): """Checks if two functions are same.""" return func1.__code__.co_code == func2.__code__.co_code def _bind(cls=None, *ags, **kw): """Internal function.\n Binds and unbinds sequences with any name given as className.""" cls = kw.pop('cls', cls) or ( ags.pop(0) if ags and isinstance(ags[0], _tk.Misc) else None) if not cls: raise ValueError('Counld not refer to any class instance "cls".') if len(kw) < 1 and len(ags) > 0: return [_bind(cls=cls, **i) for i in ags] classname = kw['className'] + str(cls) bindtags = list(cls.bindtags()) if kw.get('func'): if classname in bindtags: bindtags.remove(classname) _bind(cls, className=kw['className'], sequence=kw['sequence']) bindtags.append(classname) cls.bindtags(tuple(bindtags)) return cls.bind_class(classname, sequence=kw['sequence'], func=kw['func'], add=kw.get('add', '+')) else: if classname in bindtags: bindtags.remove(classname) cls.bindtags(tuple(bindtags)) cls.unbind_class(classname, kw['sequence']) def _on_press_color(cls=None, cnf={}, **kw): """Internal function. Do not call directly.\n Give gradient color effect used for activebackground. Returns ids""" kw = _cnfmerge((cnf, kw)) cls = kw.get('cls', cls) w = cls.cnf.get('height', cls.winfo_width()) h = cls.cnf.get('height', cls.winfo_height()) tag = kw.get('tag', 'press') state = kw.get('state', 'normal' if cls.cnf.get( 'state') in ('active', 'pressed') else 'hidden') if not cls: raise ValueError('Counld not refer to any class instance "cls".') if kw.get('color') is None: kw.pop('color', None) width = cls.coords(tag) or 0 if isinstance(width, (list, tuple)) and len(width) > 3: width = int(width[2]) all_activebg_ids = cls.find('withtag', tag) cond1 = bool(h == len(all_activebg_ids)) cond2 = bool(w == width) # [issue-1] Need a better approach for getting "cond3" cond3 = bool(kw.get('color', (False,)) == cls.cnf.get('activebackground')) if cond1 and cond2 and cond3 and not kw.get('force_create', False): return cls.cnf['activebackground'] = kw.get('color', cls.cnf.get( 'activebackground', ("#4b91fe", "#055be5"))) cls.delete(tag) ids = [] # This is the default color for mac cr = kw.get('color', ("#4b91fe", "#055be5")) if isinstance(cr, (tuple, list)): if None in cr: cr = list(cr) cr.remove(None) cr = cr[0] if not isinstance(cr, (tuple, list)): cr = (C.Color(cr), C.Color(cr)) else: cr = (C.Color(cr[0]), C.Color(cr[1])) for i, j in enumerate(tuple(cr[0].range_to(cr[1], kw.get('height', h)))): ags = (0, i, kw.get('width', w), i) cnf = {'fill': j, 'tag': tag, 'state': state} ids.append(cls._create('line', ags, cnf)) cls.tag_lower(tag) # keep the tag last return tuple(ids) def _info_button(master, cnf={}, **kw): """Internal Function.\n This function takes essentials parameters to give the approximate width and height accordingly. \n It creates a ttk button and use all the resources given and returns width and height of the ttk button, after taking width and height the button gets destroyed also the custom style.""" kw = _tk._cnfmerge((cnf, kw)) cnf = dict(**kw) kw.pop('height', None) def _get_height(master, **kw): """Creates temp tkinter button to get approximate height when -height parameter is given.""" y_dif = 2 if kw.get('pady') else 0 y_dif += -kw.get('pady', 1) + 2 if kw.get('image')\ and kw.get('compound') in ('top', 'bottom') else -2 tmp = _tk.Button(master, **kw) height = max(24, tmp.winfo_reqheight()+y_dif) tmp.destroy() return height name = '%s.TButton' % master _style_tmp = ttk.Style() _style_tmp.configure(name, font=kw.pop('font', None)) _style_tmp.configure(name, padding=(kw.pop('padx', 0), kw.pop('pady', 0))) tmp = ttk.Button(master, style=name, **kw) if cnf.get('height', None) and False: geo = [tmp.winfo_reqwidth(), _get_height(master, **cnf)] else: geo = [tmp.winfo_reqwidth(), tmp.winfo_reqheight()] # [issue-2] Need fix --- doesn't really delete the custom style del _style_tmp tmp.destroy() return geo def check_appearance(cmd='defaults read -g AppleInterfaceStyle'): """### Checks DARK/LIGHT mode of macos. Returns Boolean. #### Args: - `cmd`: Give commands. Like to check DARK/LIGHT mode \ the command is `'defaults read -g AppleInterfaceStyle'` . """ import subprocess out, err = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, shell=True).communicate() if out: return True elif err: return False def get_shade(color, shade, mode='auto'): """### Darken or Lighten a shade of color. #### Args: 1. `color`: Give a color as either HEX or name of the color. 2. `shade`: The amount of change required. Takes float \ between 0.0 to 1.0 eg: shade=0.225. 3. `mode`: - `'-'` for darker shade. - `'+'` for lighter shade. - `'auto-110'` automatically decide lighter or \ darker. where 110 is the intensity. return hexcode""" op = {'+': lambda x, y: float(x+y), '-': lambda x, y: float(x-y)} if isinstance(color, str): color = list(float(i/65535.0) for i in _tk._default_root.winfo_rgb(color)) if 'auto' in mode: intensity = (110.0 if len(mode) <= 4 else float( mode.split('-')[1])) / 1000.0 color_intensity = float( color[0]*0.299 + color[1]*0.587 + color[2]*0.114) mode = '-' if color_intensity > intensity else '+' if color_intensity > intensity*2 or color_intensity < intensity/2: shade += shade if mode not in op: raise ValueError( 'Invalid mode "{}", Takes only "-" or "+"'. format(mode)) for index, c in enumerate((op[mode](c, shade) for c in color)): if c > 1.0: c = 1.0 elif c < 0.0: c = 0.0 color[index] = int(c*255.0) return '#%02x%02x%02x' % (color[0], color[1], color[2]) class _Canvas(_tk.Widget): """Internal Class.""" def __init__(self, master=None, cnf={}, **kw): _tk.Widget.__init__(self, master, 'canvas', cnf, kw) def find(self, *args): """Internal function.""" return self._getints( self.tk.call((self._w, 'find') + args)) or () def bbox(self, *args): """Return a tuple of X1,Y1,X2,Y2 coordinates for a rectangle which encloses all items with tags specified as arguments.""" return self._getints( self.tk.call((self._w, 'bbox') + args)) or None def coords(self, *args): """Return a list of coordinates for the item given in ARGS.""" return [self.tk.getdouble(x) for x in self.tk.splitlist( self.tk.call((self._w, 'coords') + args))] def _create(self, itemType, args, kw): # Args: (val, val, ..., cnf={}) """Internal function.""" args = _tk._flatten(args) cnf = args[-1] if isinstance(cnf, (dict, tuple)): args = args[:-1] else: cnf = {} return self.tk.getint(self.tk.call( self._w, 'create', itemType, *(args + self._options(cnf, kw)))) def delete(self, *args): """Delete items identified by all tag or ids contained in ARGS.""" self.tk.call((self._w, 'delete') + args) def itemcget(self, tagOrId, option): """Return the resource value for an OPTION for item TAGORID.""" return self.tk.call( (self._w, 'itemcget') + (tagOrId, '-'+option)) def tag_lower(self, *args): """Lower an item TAGORID given in ARGS (optional below another item).""" self.tk.call((self._w, 'lower') + args) def tag_raise(self, *args): """Raise an item TAGORID given in ARGS (optional above another item).""" self.tk.call((self._w, 'raise') + args) def rounded_rect(self, ags=(), *args, **kw): 'Internal function.' x, y, w, h, c = _agsmerge((ags, args)) ids = [] cnf = dict(kw) for i in ('extent', 'start', 'style'): cnf.pop(i, None) for i in ('joinstyle', 'smooth', 'slinesteps'): kw.pop(i, None) points = ( # Arc points:- (x, y, x+2*c, y+2*c), (x, y+h-2*c, x+2*c, y+h), (x+w-2*c, y+h-2*c, x+w, y+h), (x+w-2*c, y, x+w, y+2*c), # Polygon points:- (x+c, y, x+w-c, y), (x+c, y+h, x+w-c, y+h), (x, y+c, x, y+h-c), (x+w, y+c, x+w, y+h-c)) for i in range(len(points)): if i <= 3: kw['start'] = 90*(i+1) ids.append(self._create('arc', points[i], kw)) else: ids.append(self._create('polygon', points[i], cnf)) return tuple(ids) def _rounded(self, ags=(), *args, **kw): x1, y1, x2, y2, c = _agsmerge((ags, args)) ids = [] points = ( # Arc points:- (x2-c-1, y1, x2-1, y1+c), (x1, y1, x1+c, y1+c), (x1, y2-c-1, x1+c, y2-1), (x2-c, y2-c, x2-1, y2-1), # Rectangle points:- (x1+c/2, y1, x2-c/2, y2), (x1, y1+c/2, x2, y2-c/2)) kw['start'], kw['outline'] = 0, '' for i in range(len(points)): if i <= 3: ids.append(self._create('arc', points[i], kw)) kw['start'] += 90 else: kw.pop('start', None) kw['width'] = 0 ids.append(self._create('rectangle', points[i], kw)) return tuple(ids) class _BaseWidget(_Canvas): """Internal class.\n Settings class for Widgets. Do not call directly.""" _buttons = [] # list of all buttons _features = ('overbackground', 'overforeground', 'activeimage', 'activebitmap', 'anchor', 'bitmap', 'bordercolor', 'borderless', 'command', 'compound', 'disabledforeground', 'justify', 'disabledbackground', 'fg', 'font', 'foreground', 'height', 'image', 'overrelief', 'padx', 'pady', 'repeatdelay', 'repeatinterval', 'text', 'textvariable', 'underline', 'width', 'state', 'focusthickness', 'focuscolor', 'highlightbackground', 'activebackground', 'activeforeground') def _mouse_state_condition(self): """Internal function.\n True if state is normal and cursor is on the widget.""" con1 = bool(self.cnf.get('state') not in 'disabled') con2 = bool(self.winfo_containing(*self.winfo_pointerxy()) == self) return con1 and con2 def _make_dictionaries(self, cnf={}, kw={}): """Internal function.\n Merges kw into cnf and removes None values.""" kw['bordercolor'] = kw['highlightbackground'] = kw.get('bordercolor', kw.get('highlightbackground')) kw = {k: v for k, v in kw.items() if v is not None} cnf.update(kw) cnf = {k: v for k, v in cnf.items() if v is not None} cnf['fg'] = cnf['foreground'] = kw.get('foreground', kw.get('fg', cnf.get('fg', 'black'))) if cnf.get('textvariable', '') != '': cnf['text'] = cnf['textvariable'].get() if kw.get('activebackground'): cnf.pop('activebackground') return cnf, kw def _set_trace(self, kw): """Internal function.""" for i in ('overforeground', 'foreground', 'fg', 'activeforeground'): if isinstance(kw.get(i), _tk.Variable): var = kw[i] cbname = var.trace_variable('w', lambda a, b, c, i=i, var=var, cls=self: cls.config({i: var.get()})) if (self, i) in tkv._all_traces_colorvar: v, cb = tkv._all_traces_colorvar.get((self, i)) v.trace_vdelete('w', cb) tkv._all_traces_colorvar[(self, i)] = (var, cbname) else: tkv._all_traces_colorvar[(self, i)] = (var, cbname) kw[i] = var.get() return kw def _create_items(self, cmd, safe_create=False, **kw): """Internal function.\n Checks and creates (text, image, bitmap, border, bordercolor, takefocus ring*) items.""" def check_tag(tag): """Internal function.\n If `cmd="check"` and the tag does not exist then the tag is created, but if `cmd="create"` and safe_create=True this will delete the tag if exists and creates a new tag with same settings.""" if cmd == 'check': c = False elif cmd == 'create': c = True else: raise ValueError( '%s is not a valid command! Takes -create, -check' % cmd) cond1 = bool(not self.find('withtag', tag) or c) cond2 = bool(tag not in kw.get('avoid', [])) if safe_create and cond1 and cond2: self.delete(tag) return cond1 and cond2 ids = [] cond_image = bool(self.cnf.get('image', self.cnf.get('activeimage', '') != '')) cond_bitmap = bool(self.cnf.get('bitmap', self.cnf.get('activebitmap', '') != '')) # Text item. if check_tag('_txt') and self.cnf.get('text'): fill = self.cnf.get('foreground', 'black') if self['state'] in ('pressed', 'active'): fill = self.cnf.get('activeforeground', 'white') ids.append(self._create('text', (0, 0), { 'text': None, 'tag': '_txt', 'fill': fill})) # Image item (image and activeimage). if check_tag('_img') and cond_image: img = self.cnf.get('image', '') if self['state'] in ('pressed', 'active'): img = self.cnf.get('activeimage', img) ids.append(self._create('image', (0, 0), { 'image': None, 'tag': '_img', 'image': img})) # Bitmap items (bitmap and activebitmap). if check_tag('_bit') and cond_bitmap: bit = self.cnf.get('image', '') if self['state'] in ('pressed', 'active'): bit = self.cnf.get('activeimage', bit) ids.append(self._create('bitmap', (0, 0), { 'bitmap': None, 'tag': '_bit', 'bitmap': bit})) # Border color item. if check_tag('_bd_color'): bd_color = self.cnf.get('bordercolor', get_shade(self['bg'], 0.04, 'auto-120')) if self._type == 'circle': pad = 2 width = r = int(self.cnf.get('width', 87)/2) # radius = x = y (in pixels) _bd_points = (pad-width, pad-width, r*2+width-pad, r*2+width-pad) kw_bd_color = { 'tag': '_bd_color', 'state': 'hidden' if self['state'] in ('pressed', 'active') else 'normal', 'width': width*2,'outline': bd_color} ids.append(self._create('oval', _bd_points, kw_bd_color)) else: _bd_points = (0, -1, self.cnf.get('width', 87), self.cnf.get('height', 24)+3, 7) # was 6 ids.append(self.rounded_rect( _bd_points, width=6, tag='_bd_color', style='arc', outline=bd_color)) # Border item. if check_tag('_border'): bo_color = get_shade(self['bg'], 0.1, 'auto-120') if self._type == 'circle': pad = 2 r = int(self.cnf.get('width', 87)/2) # radius = x = y _bo_points = (pad, pad, r*2-(pad+1), r*2-(pad+1)) ids.append(self._create('oval', _bo_points, { 'tag': '_border', 'outline': self.cnf.get('bordercolor', bo_color) })) else: h = 4 if int(self['highlightthickness']): h += 1 _bo_points = (2, 2, self.cnf.get('width', 87)-5, self.cnf.get('height', 24)-h, 4) # was 3 ids.append(self.rounded_rect( _bo_points, width=1, outline=bo_color, smooth=1, tag='_border', style='arc', state='hidden' if self['state'] in ('pressed', 'active') else 'normal')) # Takefocus highlight ring. if check_tag('_tf'): if self._type == 'circle': pad = 1 width = self.cnf.get('focusthickness', 2) r = int(self.cnf.get('width', 87)/2) # radius = x = y _tk_points = (pad+width, pad+width, r*2-width-pad, r*2-width-pad) ids.append(self._create('oval', _tk_points, { 'tag': '_tf', 'width': width, 'outline': self.cnf.get('focuscolor', '#81b3f4')})) else: # takefocuswidth can be changed. # Focus line is not on point the line is off when thickness is changed. s = w = self.cnf.get('focusthickness', 2) if s == 1: s = 2 diff2 = (int(self['highlightthickness'])*2) + (s*2) diff1 = diff2 + 1 else: diff1 = diff2 = (int(self['highlightthickness'])*2) + (s*2) if diff2 == (s*2): diff2 -= 1 _tk_points = (s+int(self['highlightthickness']), s+int(self['highlightthickness']), self.cnf.get('width', 87)-diff1, self.cnf.get('height', 24)-diff2, 4) ids.append(self.rounded_rect( _tk_points, width=w, style='arc', outline=self.cnf.get('focuscolor', '#81b3f4'), tag='_tf', state='hidden')) return tuple(ids) def _get_functions(self, cmds, kw={}): """Internal function.\n Contains all the required functions.""" def _borderless_support(cnf={}, **kw): _return = self.master._configure('configure', cnf, kw) kw = _cnfmerge((cnf, kw)) if kw.get('bg') or kw.get('background'): for i in self._buttons: if i['borderless']: i.cnf['highlightbackground'] = i.cnf['bordercolor'] = i.master['bg'] _Canvas._configure(i, ('itemconfigure', '_bd_color'), { 'outline': i.master['bg']}, None) _Canvas._configure(i, 'configure', { 'highlightbackground': i.master['bg']}, None) return _return def if_state(fn): """Runs function only if state is normal.""" def wrapper(*a, **k): if self.cnf.get('state') not in 'disabled': return fn(*a, **k) return wrapper def over_img_bit(seq, key): """Resets coords for text with overimage/overbitmap.""" if self.cnf['state'] in ('active', 'pressed'): return tag = '_img' if key == 'image' else '_bit' if seq == 'enter': _Canvas._configure(self, ('itemconfigure', tag), {key: kw.get('over'+key)}, None) elif seq == 'leave': _Canvas._configure(self, ('itemconfigure', tag), {key: self.cnf.get(key, '')}, None) self._set_coords(self._get_options( ('_txt', '_img', '_bit'), self.cnf)) def overbg(seq): """Implement overbackground properly.""" if seq == 'enter': _Canvas._configure(self, 'configure', {'bg': kw.get('overbackground')}, None) _Canvas._configure(self, ('itemconfigure', '_border'), {'outline': get_shade(kw.get('overbackground'), 0.1, 'auto-120')}, None) elif seq == 'leave': self._configure('configure', {'bg': self._org_bg}, None) def overfg(seq): """Handle overforeground, foreground and activeforeground.""" if self.cnf['state'] in ('active', 'pressed'): return if seq == 'enter': _Canvas._configure(self, ('itemconfigure', '_txt'), {'fill': self.cnf.get('overforeground', 'black')}, None) elif seq == 'leave': _Canvas._configure(self, ('itemconfigure', '_txt'), {'fill': self.cnf.get('fg', 'black')}, None) binds = { 'overrelief': {'': if_state(lambda _: self._configure( 'configure', {'_relief': kw.get('overrelief')})), '': if_state(lambda _: self._configure( 'configure', {'_relief': self._rel[0]}))}, # [issue-3] doesn't change if overrelief is on. # [issue-3] (FIXED) using after with 0ms delay fixes the issue. To be safe delay is 1ms. 'overforeground': {'': if_state(lambda _: self.after(1, overfg, 'enter')), '': if_state(lambda _: overfg('leave'))}, 'overbackground': {'': if_state(lambda _: overbg('enter')), '': if_state(lambda _: overbg('leave')) }, 'overimage': {'': if_state(lambda _: over_img_bit('enter', 'image')), '': if_state(lambda _: over_img_bit('leave', 'image'))}, 'overbitmap': {'': if_state(lambda _: over_img_bit('enter', 'bitmap')), '': if_state(lambda _: over_img_bit('leave', 'bitmap'))}, 'takefocus': {'': lambda _: _Canvas._configure(self, ( 'itemconfigure', '_tf'), {'state': 'normal'}, None), '': lambda _: _Canvas._configure(self, ( 'itemconfigure', '_tf'), {'state': 'hidden'}, None)}, } other_functions = { 'borderless': _borderless_support, 'textvariable': {'w': lambda *a: _Canvas._configure(self, ( 'itemconfigure', '_txt'), {'text': self.cnf.get( 'textvariable').get()}, None)}, '_activebg': lambda: _on_press_color(*self._get_options( 'activebackground', self.cnf)), } funcs = _cnfmerge((binds, other_functions)) if isinstance(cmds, (list, tuple)): value = funcs.copy() for i in cmds: value = value.get(i, {}) return value else: return funcs.get(cmds, {}) def _get_options(self, cmd, cnf={}, **kw): """Internal function.""" kw = _cnfmerge((cnf, kw)) # If more than one commands are given. # returns Union[list, dict, tuple(list, dict)] if isinstance(cmd, (list, tuple)): ags, cnf = [], {} for i in cmd: opts = self._get_options(i, kw) if isinstance(opts, (list, tuple)): ags.append(opts) elif isinstance(opts, dict): cnf.update(opts) if isinstance(opts, (list, tuple)) and isinstance(opts, dict): return ags, cnf return ags or cnf elif cmd == 'activebackground': color = None if kw.get(cmd, '') != '': color = kw.get(cmd, self.cnf.get(cmd)) return [self, {'tag': '_activebg', 'width': self.winfo_width(), 'height': self.winfo_height(), 'color': color}] elif cmd == 'activeforeground': if self['state'] in ('pressed', 'active'): return (('itemconfigure', '_txt'), { 'fill': self.cnf.get(cmd, 'white')}, None) elif cmd == 'activebitmap' or cmd == 'activeimage': tag = '_bit' if cmd == 'activebitmap' else '_img' name = cmd.rsplit('active')[-1] if self['state'] in ('pressed', 'active'): return (('itemconfigure', tag), { name: self.cnf.get(cmd, '')}, None) elif cmd == 'bitmap' or cmd == 'image': tag = '_bit' if cmd == 'bitmap' else '_img' if kw.get(cmd, '') != '': state = 'disabled' if self.cnf.get('state') in ( 'disabled', 'disable') else 'normal' cnf = dict(state=state, **{i: kw.get(i, self.cnf.get(i)) for i in ('anchor', cmd)}) return (('itemconfigure', tag), cnf, None) elif kw.get(cmd) == '': self.delete(tag) elif cmd == 'borderless' or cmd == 'bordercolor' or cmd == 'highlightbackground': cnf = {} if bool(kw.get('borderless')) or self.cnf.get('borderless'): if not check_function_equality(self.master.config, self._get_functions('borderless', kw)): self.master.config = self.master.configure = self._get_functions( 'borderless', kw) self.cnf['highlightbackground'] = self.cnf['bordercolor'] = self.master['bg'] cnf[1] = [('itemconfigure', '_bd_color'), {'outline': self.master['bg']}, None] cnf[2] = ['configure', {'highlightbackground': self.master['bg']}, None] return cnf elif not bool(kw.get('borderless', True)) or not self.cnf.get('borderless'): if self.cnf.get('bordercolor') == self.master['bg']: self.cnf.pop('bordercolor', None) self.cnf.pop('highlightbackground', None) bd_color = self.cnf.get( 'bordercolor', get_shade(self['bg'], 0.04, 'auto-120')) if bd_color == '': bd_color = get_shade(self['bg'], 0.04, 'auto-120') elif bd_color.lower() == 'default': bd_color = get_shade('white', 0.04, 'auto-120') self.cnf.update({'bordercolor': bd_color}) self.cnf['highlightbackground'] = self.cnf['bordercolor'] = bd_color if self.itemcget('_bd_color', 'outline') != bd_color: cnf[1] = [('itemconfigure', '_bd_color'), {'outline': bd_color}, None] cnf[2] = ['configure', {'highlightbackground': bd_color}, None] return cnf elif cmd == 'foreground' or cmd == 'fg': return (('itemconfigure', '_txt'), {'fill': kw.get(cmd, self.cnf.get(cmd))}, None) elif cmd == 'focuscolor': return (('itemconfigure', '_tf'), {'outline': kw.get(cmd, self.cnf.get(cmd))}, None) elif cmd == 'focusthickness': self._create_items('create', True, avoid=( '_txt', '_img', '_bit', '_bd_color', '_border')) elif cmd == 'overbackground': cnf = [] if kw.get(cmd, '') != '': fn = self._get_functions(cmd, kw) cnf = [self, {'className': cmd, 'sequence': '', 'func': fn.get('')}, {'className': cmd, 'sequence': '', 'func': fn.get('')}] if self._mouse_state_condition(): _Canvas._configure(self, 'configure', {'bg': kw.get('overbackground')}, None) _Canvas._configure(self, ('itemconfigure', '_border'), {'outline': get_shade(kw.get('overbackground'), 0.1, 'auto-120')}, None) elif kw.get(cmd) == '': cnf = [self, {'className': cmd, 'sequence': ''}, {'className': cmd, 'sequence': ''}, ('configure', {'bg': self._org_bg}, None), (('itemconfigure', '_border'), {'outline': get_shade(self._org_bg, 0.1, 'auto-120')}, None)] return cnf elif cmd == 'overforeground': cnf = [] if kw.get(cmd, '') != '': fn = self._get_functions(cmd, kw) cnf = [self, {'className': cmd, 'sequence': '', 'func': fn.get('')}, {'className': cmd, 'sequence': '', 'func': fn.get('')}] if self._mouse_state_condition(): # [issue-4] doesn't change if overrelief is on. # [issue-4] (FIXED) using after with 0ms delay # fixes the issue. To be safe delay is 1ms. self.after(1, lambda: _Canvas._configure(self, ('itemconfigure', '_txt'), {'fill': kw.get(cmd)}, None)) elif kw.get(cmd) == '': cnf = [self, {'className': cmd, 'sequence': ''}, {'className': cmd, 'sequence': ''}] cnf.append((('itemconfigure', '_txt'), {'fill': self.cnf.get('fg', 'black')}, None)) return cnf elif cmd == 'overrelief': if kw.get(cmd, '') != '': if not self._rel[1]: self._rel = ('flat', False) if self._mouse_state_condition(): self._configure( 'configure', {'_relief': kw.get(cmd)}, None) fn = self._get_functions(cmd, kw) return (self, {'className': cmd, 'sequence': '', 'func': fn.get('')}, {'className': cmd, 'sequence': '', 'func': fn.get('')}) elif kw.get(cmd) == '': return (self, {'className': cmd, 'sequence': ''}, {'className': cmd, 'sequence': ''}, ('configure', {'relief': self._rel[0]}, None)) elif cmd == 'size': cnf = {} for i in ('text', 'font', 'textvariable', 'image', 'bitmap', 'compound', 'padx', 'pady', 'activeimage', 'activebitmap'): # 'width', 'height'}: if self.cnf.get(i, '') != '': if i == 'activeimage': cnf['image'] = self.cnf[i] elif i == 'activebitmap': cnf['bitmap'] = self.cnf[i] elif i in ('width', 'height'): cnf[i] = str(self.cnf[i]) + 'c' else: cnf[i] = self.cnf.get(i) if self._type == 'circle' and kw.get('radius'): self.cnf['width'] = self.cnf['height'] = kw.get( 'width', kw.get('height', int(kw['radius']*2))) if self._fixed_size['w'] and kw.get('width', True): kw['width'] = self.cnf['width'] if self._fixed_size['h'] and kw.get('height', True): kw['height'] = self.cnf['height'] self._fixed_size['w'] = True if kw.get('width', kw.get('radius')) else False self._fixed_size['h'] = True if kw.get('height', kw.get('radius')) else False W, H = _info_button(self, **cnf) self.cnf['width'] = self.cnf.get('width') if self._fixed_size['w'] else W self.cnf['height'] = self.cnf.get('height') if self._fixed_size['h'] else H return ('configure', {'width': self.cnf['width'], 'height': self.cnf['height']}, None) elif cmd == 'state': cnf = {} if kw.get(cmd) in 'disabled': cnf[1] = ('configure', {'bg': self.cnf.get('disabledbackground'), 'state': 'disabled'}, None) cnf[2] = (('itemconfigure', '_txt'), { 'disabledfill': kw.get('disabledforeground', self.cnf.get('disabledforeground')), 'state': 'disabled'}, None) cnf[3] = (('itemconfigure', '_activebg'), {'state': 'hidden'}, None) cnf[5] = (('itemconfigure', '_img'), {'state': 'disabled'}, None) cnf[6] = (('itemconfigure', '_bit'), {'state': 'disabled'}, None) elif kw.get(cmd) == 'normal': _bg = self._org_bg if self._mouse_state_condition() and self.cnf.get('overbackground'): _bg = self.cnf['overbackground'] cnf[1] = ('configure', {'bg': _bg, 'state': 'normal'}, None) cnf[2] = (('itemconfigure', '_txt'), {'state': 'normal', 'fill': self.cnf.get('foreground', 'black')}, None) cnf[3] = (('itemconfigure', '_activebg'), {'state': 'hidden'}, None) cnf[4] = (('itemconfigure', '_border'), {'state': 'normal'}, None) cnf[5] = (('itemconfigure', '_img'), { 'image': self.cnf.get('image', '')}, None) cnf[6] = (('itemconfigure', '_bit'), { 'bitmap': self.cnf.get('bitmap', '')}, None) elif kw.get(cmd) in ('active', 'pressed'): _bg = self._org_bg if self._mouse_state_condition() and self.cnf.get('overbackground'): _bg = self.cnf['overbackground'] cnf[1] = ('configure', {'bg': _bg, 'state': 'normal'}, None) cnf[2] = (('itemconfigure', '_txt'), {'state': 'normal', 'fill': self.cnf.get('activeforeground', 'white')}, None) cnf[3] = (('itemconfigure', '_activebg'), {'state': 'normal'}, None) cnf[4] = (('itemconfigure', '_border'), {'state': 'hidden'}, None) cnf[5] = (('itemconfigure', '_img'), { 'image': self.cnf.get('activeimage', '')}, None) cnf[6] = (('itemconfigure', '_bit'), { 'bitmap': self.cnf.get('activebitmap', '')}, None) return cnf elif cmd == 'takefocus': if int(self[cmd]) and self['state'] in ('normal', 'active', 'pressed'): fn = self._get_functions(cmd, kw) return [self, {'className': cmd, 'sequence': '', 'func': fn.get('')}, {'className': cmd, 'sequence': '', 'func': fn.get('')}] elif not int(self[cmd]) or self['state'] in 'disabled': return [self, {'className': cmd, 'sequence': ''}, {'className': cmd, 'sequence': ''}, (('itemconfigure', '_tf'), {'state': 'hidden'}, None)] elif cmd == 'text': if kw.get('textvariable', '') != '': kw['text'] = self.cnf['text'] = self.cnf['textvariable'].get() cbn = self.cnf['textvariable'].trace_variable('w', self._get_functions('textvariable', kw).get('w')) self._var_cb = (self.cnf['textvariable'], cbn) elif kw.get('textvariable') == '' and self._var_cb: kw['text'] = self.cnf['text'] = self._var_cb[0].get() self.cnf.pop('textvariable', None) self._var_cb[0].trace_vdelete('w', self._var_cb[1]) self._var_cb = [] state = 'disabled' if self.cnf.get('state') in ( 'disabled', 'disable') else 'normal' fill = kw.get('fg', self.cnf.get('fg')) if self['state'] in ('pressed', 'active'): fill = self.cnf.get('activeforeground', 'white') cnf = dict(state=state, fill=fill, disabledfill=kw.get( 'disabledforeground', self.cnf.get('disabledforeground')), **{i: kw.get(i, self.cnf.get(i)) for i in ('text', 'anchor', 'font', 'justify')}) return (('itemconfigure', '_txt'), cnf, None) elif cmd == '_border': return [('itemconfigure', '_border'), {'outline': get_shade(self['bg'], 0.1, 'auto-120')}, None] elif cmd == '_txt' or cmd == '_img' or cmd == '_bit': if not self.cnf.get('width'): self.cnf['width'] = self.winfo_width() if not self.cnf.get('height'): self.cnf['height'] = self.winfo_height() r1 = self._compound(self.cnf.get('compound'), self.cnf.get('width'), self.cnf.get('height')) r2 = {cmd: ((self.cnf['width']/2), self.cnf['height']/2)} return r1 or r2 def _set_coords(self, cnf={}, **kw): """Internal function.\n Set Coordinates of the items.""" kw = _cnfmerge((cnf, kw)) return [self.coords(tag, *kw[tag]) for tag in ('_txt', '_img', '_bit', '_bd_color', '_border', '_tf') if kw.get(tag)] def _set_configure(self, options): """Internal function.\n Configures and binds according to the given options.""" if not options: return if isinstance(options, dict): return [self._set_configure(i) for i in options.values()] if len(options) > 1 and isinstance(options[1], dict): con1 = bool(isinstance(options[0], tuple)) if options[0] == 'configure' or (con1 and len(options[0]) > 1): # itemconfigure and configure _Canvas._configure(self, *options) if isinstance(options[0], _tk.Misc): if isinstance(options[-1], (tuple, list)): # binds, itemconfigure and configure binds, conf = options[:-2], options[-1] _Canvas._configure(self, *conf) return _bind(*binds) elif options[1].get('tag') == '_activebg': # _on_press_color return _on_press_color(*options) else: # binds return _bind(*options) def _configure1(self, cnf={}, **kw): """Internal Function. This function configure all the resources of the Widget and save it to the class dict.""" self.cnf, kw = self._make_dictionaries(self.cnf, self._set_trace(_cnfmerge((cnf, kw)))) # Checks the items self._create_items('check') # >.< for opt in ('overbackground', 'activebitmap', 'activeimage', 'bitmap', 'fg', 'bordercolor', 'borderless', 'image', 'overrelief', 'foreground', 'state', 'overforeground', 'focuscolor', 'focusthickness', 'activebackground', 'activeforeground', 'compound'): if kw.get(opt) is not None: self._set_configure(self._get_options(opt, kw)) if (opt == 'state' and kw.get('compound', self.cnf.get('compound'))) \ or opt == 'compound': self._set_coords(**self._get_options( ('_txt', '_img', '_bit'), kw)) # Size if bool({'text', 'font', 'textvariable', 'image', 'bitmap', 'compound', 'padx', 'pady', 'width', 'height', 'activeimage', 'radius', 'activebitmap',}.intersection(set(kw))): self._set_configure(self._get_options('size', kw)) # Text if {'text', 'anchor', 'font', 'justify', 'textvariable'}.intersection(set(kw)): self._set_configure(self._get_options('text', kw)) # Takefocus cond1 = bool(self.itemcget('_tf', 'state') != 'hidden' and self['state'] not in 'disabled') cond2 = bool(self.itemcget('_tf', 'state') != self['state']) if cond1 or cond2: self._set_configure(self._get_options('takefocus', kw)) # Line border: This will darken the border around the button. if get_shade(self['bg'], 0.1, 'auto-120') != self.itemcget('_border', 'outline'): self._set_configure(self._get_options('_border', kw)) def _focus_in_out(self, intensity): """Internal function.\n Focus in and focus out effect maker.""" main_win = self.winfo_toplevel() def _chngIn(evt): """Internal function.""" try: if self.focus_get() is None: color = get_shade(self['bg'], intensity, 'auto-120') _Canvas._configure(self, ('itemconfigure', '_border'), {'outline': color}, None) c1 = get_shade(self['bg'], intensity, 'auto-120') c2 = self.itemcget('_border', 'outline') if self.focus_get() and c1 == c2: color = get_shade(self['bg'], 0.1, 'auto-120') _Canvas._configure(self, ('itemconfigure', '_border'), {'outline': color}, None) # [issue-8] (Fixed) tkinter issue with combobox (w = w.children[n]) except KeyError: pass _bind(main_win, {'className': 'focus%s' % str(self), 'sequence': '', 'func': _chngIn}, {'className': 'focus%s' % str(self), 'sequence': '', 'func': _chngIn}) return main_win def _relief(self, cnf, kw={}): # check """Internal function.\n Make overrelief and Relief work together.""" # [issue-5] (Fixed) overrelief and Relief not working together properly. kw = _cnfmerge((cnf, kw)) cond1 = kw.get('overrelief', self.cnf.get('overrelief', '')) != '' if kw.get('relief') is not None and cond1: self._rel = (kw['relief'], True) if kw.get('_relief') is not None and kw.get('relief') is not None: self._rel = (kw['relief'], True) kw.pop('_relief', None) elif kw.get('_relief'): kw['relief'] = kw.pop('_relief') return kw def _set_size(self, evt=None): """Internal function.\n This will resize everything that is in the button""" if evt.width == self._size[0] and evt.height == self._size[1]: return # [issue-8] On resizing the window the circlebutton doesn't resize properly, # current fix doesn't work properly. if self._type == 'circle' and evt.width != evt.height: if evt.width > evt.height: evt.width = evt.height else: evt.height = evt.width return self._size = (self.cnf['width'], self.cnf['height']) = (evt.width, evt.height) self.delete('_activebg') # [issue-6] (Fixed) Need fix (laggy on resizing) --> workaround: cancel if still resizing for i in self._after_IDs: self.after_cancel(self._after_IDs[i]) self._create_items('create', avoid=( '_txt', '_img', '_bit'), safe_create=True) self._set_coords(self._get_options(('_txt', '_img', '_bit'), self.cnf)) self._after_IDs[1] = self.after(1, self._get_functions('_activebg')) for t in ('_txt', '_img', '_bit', '_bd_color', '_border', '_tf'): self.tag_raise(t) self._after_IDs[2] = self.after(1, self._configure1) cur_focus = self.master.focus_get() if cur_focus and self.master != cur_focus and cur_focus != self: cur_focus.focus() else: self.master.focus() # [issue-7] Need a better fix to get focus back to the button if it has it previously if cur_focus == self: # [issue-7] (fixed) using after with 1ms delay solves the issue. self._after_IDs[3] = self.after(1, self.focus) def _active(self, value): """Internal function.\n Do not call directly. Changes appearance when active.""" if value in ('on_press', 'on_enter') or value == True: if self['state'] != 'pressed' : if value == 'on_press': self.cnf['_state'] = self.cnf['state'] self.cnf['state'] = 'active' _Canvas._configure(self, ('itemconfigure', '_activebg'), {'state': 'normal'}, None) _Canvas._configure(self, ('itemconfigure', '_border'), {'state': 'hidden'}, None) _Canvas._configure(self, ('itemconfigure', '_txt'), {'fill': self.cnf.get('activeforeground', 'white')}, None) _Canvas._configure(self, ('itemconfigure', '_img'), {'image': self.cnf.get('activeimage', self.cnf.get('image', ''))}, None) _Canvas._configure(self, ('itemconfigure', '_bit'), {'bitmap': self.cnf.get('activebitmap', self.cnf.get('bitmap', ''))}, None) elif value in ('on_leave', 'on_release') or value == False: # When not active (False) if self['state'] != 'pressed': if self.cnf.get('_state'): self.cnf['state'] = self.cnf['_state'] if value == 'on_release': self.cnf.pop('_state', None) if self['state'] == 'active': self['state'] = 'normal' _Canvas._configure(self, ('itemconfigure', '_activebg'), {'state': 'hidden'}, None) _Canvas._configure(self, ('itemconfigure', '_border'), {'state': 'normal'}, None) fill = self.cnf.get('fg', 'black') if self._mouse_state_condition() and self.cnf.get('overforeground'): fill = self.cnf['overforeground'] _Canvas._configure(self, ('itemconfigure', '_txt'), {'fill': fill}, None) _Canvas._configure(self, ('itemconfigure', '_img'), {'image': self.cnf.get('image', '')}, None) _Canvas._configure(self, ('itemconfigure', '_bit'), {'bitmap': self.cnf.get('bitmap', '')}, None) # if self.cnf.get('compound'): self._set_coords(self._get_options( ('_txt', '_img', '_bit'), self.cnf)) def _on_press(self, *ags): """Internal function. When button is pressed """ self._rpin = None self._rpinloop = True def cmd(*a): """trigger function callback.""" _bind(self, className='button_command', sequence='') if self.cnf.get('repeatdelay', 0) and self.cnf.get('repeatinterval', 0) and self._rpinloop: self._rpin = self.after(self.cnf.get('repeatinterval', 0), cmd) self.cnf['command']() if self.cnf.get('command') else None def on_enter(*a): """Internal function.\n Enables when pressed and cursor is moved back on button.""" self._active('on_enter') if self.cnf.get('repeatdelay', 0) and self.cnf.get('repeatinterval', 0): self._rpinloop = True cmd() _bind(self, className='button_command', sequence='', func=lambda *a: self.after(0, cmd), add='+') def on_leave(*a): """Internal function.\n Disables/Cancels when pressed and cursor is moved away from the button.""" self._active('on_leave') if self.cnf.get('repeatdelay', 0) and self.cnf.get('repeatinterval', 1): self._rpinloop = False self.after_cancel(self._rpin) _bind(self, className='button_command', sequence='') if self['state'] not in 'disabled': self.focus_set() self._active('on_press') if self.cnf.get('repeatdelay', 0) and self.cnf.get('repeatinterval', 1): self._rpin = self.after(self.cnf.get('repeatdelay', 0), cmd) _bind(self, {'className': 'on_press_enter', 'sequence': '', 'func': on_enter}, {'className': 'on_press_leave', 'sequence': '', 'func': on_leave}, {'className': 'button_command', 'sequence': '', 'func': cmd}) def _on_release(self, *ags): """Internal function. When button is released """ if self['state'] in 'disabled': return self._active('on_release') self._rpinloop = False if getattr(self, '_rpin', None): self.after_cancel(self._rpin) _bind(self, {'className': 'on_press_enter', 'sequence': ''}, {'className': 'on_press_leave', 'sequence': ''}, {'className': 'button_command', 'sequence': ''}) def _compound(self, flag, width, height): """Internal function.\n Use `compound = 'left'/'right'/'top'/'bottom'` to configure.""" # width -= 2 _PiTag = '' if self.cnf.get('image', self.cnf.get('activeimage')): _PiTag = '_img' elif self.cnf.get('bitmap', self.cnf.get('activebitmap')): _PiTag = '_bit' _im_size = self.bbox(_PiTag) _txt_size = self.bbox('_txt') if _im_size and _txt_size: W_im = _im_size[2] - _im_size[0] H_im = _im_size[3] - _im_size[1] W_txt = _txt_size[2] - _txt_size[0] H_txt = _txt_size[3] - _txt_size[1] if flag == 'bottom': width = (width/2, width/2) height = (height/2-H_im/2, height/2+H_txt/2) elif flag == 'top': width = (width/2, width/2) height = (height/2+H_im/2, height/2-H_txt/2) elif flag == 'right': width = (width/2-W_im/2, width/2+W_txt/2) height = (height/2, height/2) elif flag == 'left': width = (width/2+W_im/2, width/2-W_txt/2) height = (height/2, height/2) elif flag is not None: raise _tk.TclError('bad compound flag "{}", must be \ -none, -top, -bottom, -left, or -right'.format(flag)) if isinstance(height, tuple): if _im_size is None: return {'_txt': (width[0], height[0])} return {'_txt': (width[0], height[0]), _PiTag: (width[1], height[1])} return None class ButtonBase(_BaseWidget): """Internal class used for tkinter macos Buttton""" def __init__(self, _type=None, master=None, cnf={}, **kw): kw = self._set_trace(_cnfmerge((cnf, kw))) kw = {k: v for k, v in kw.items() if v is not None} self._type = _type # button type (circle, normal) self._after_IDs = {} # _after_IDs self._fixed_size = {'w': False, 'h': False} self._var_cb = None self.cnf = {} for i in kw.copy().keys(): if i in self._features: self.cnf[i] = kw.pop(i, None) self.cnf['fg'] = self.cnf['foreground'] = self.cnf.get('fg', self.cnf.get('foreground', 'black')) self.cnf['anchor'] = self.cnf.get('anchor', 'center') self.cnf['borderless'] = self.cnf.get('borderless', False) self.cnf['disabledforeground'] = self.cnf.get('disabledforeground', 'grey') self.cnf['state'] = self.cnf.get('state', 'normal') self.cnf['activeforeground'] = self.cnf.get('activeforeground', 'white') if self._type == 'circle': self.cnf['radius'] = int(kw.pop('radius', 35)) ra = int(self.cnf['radius']*2 + 4) kw['width'] = kw['height'] = kw.get('width', kw.get('height', ra)) else: kw['width'] = kw.get('width', 87) kw['height'] = kw.get('height', 24) kw['takefocus'] = kw.get('takefocus', 1) kw['bg'] = kw.pop('bg', kw.pop('background', 'white')) kw['highlightthickness'] = kw.get('highlightthickness', 0) _Canvas.__init__(self, master=master, **kw) self.cnf['bordercolor'] = self.cnf['highlightbackground'] = self.cnf.get( 'bordercolor', self.cnf.get('highlightbackground', get_shade(self['bg'], 0.04, 'auto-120'))) self._buttons.append(self) self._size = (self.winfo_width(), self.winfo_height()) self._create_items('create', safe_create=True) self._org_bg = self['bg'] if kw.get('relief') is not None: self._rel = (kw['relief'], True) else: self._rel = ('flat', False) _bind(self, {'className': 'button_release', 'sequence': '', 'func': self._on_release}, {'className': 'button_press', 'sequence': '', 'func': self._on_press}, {'className': 'set_size', 'sequence': '', 'func': self._set_size}) self._focus_in_out(0.04) self._configure1(self.cnf) def _configure(self, cmd, cnf=None, kw=None): 'Internal function to configure the inherited class' kw = self._relief(cnf, kw) cnf = {} for i in list(kw): if (i in self._features) or \ (i == 'radius' and self._type == 'circle'): cnf[i] = kw.pop(i, None) _return = _Canvas._configure(self, cmd, None, kw) if kw.get('bg') or kw.get('background'): self._org_bg = self['bg'] self._configure1(cnf) if _return is not None and isinstance(_return, dict): _return.update(self.cnf) return _return # @tkv._colorvar_patch_cget def cget(self, key): """Return the resource value for a KEY given as string.""" if key == 'radius' and self._type == 'circle': return self.cnf.get('radius') if key in self._features: return self.cnf.get(key) else: return _Canvas.cget(self, key) __getitem__ = cget def keys(self): """Return a list of all resource names of this widget.""" _return = _Canvas.keys(self) _return.extend(self._features) return sorted(list(set(_return))) @tkv._colorvar_patch_destroy def destroy(self): """Destroy this and all descendants widgets. This will end the application of this Tcl interpreter.""" main_win = self.winfo_toplevel() _bind(main_win, {'className': 'focus%s' % str(self), 'sequence': ''}, {'className': 'focus%s' % str(self), 'sequence': ''}) if self.cnf.get('textvariable', '') != '': self.configure(textvariable='') if self in self._buttons: self._buttons.remove(self) return _Canvas.destroy(self) class SFrameBase(_tk.Frame): """Base Class for SFrame.""" _features = ('scrollbarwidth', 'mousewheel', 'avoidmousewheel', 'canvas', 'scrollbar') def __init__(self, master=None, cnf={}, **kw): kw = _cnfmerge((cnf, kw)) self.cnf = {} self._after_ids = {} self.cnf['scrollbarwidth'] = kw.pop('scrollbarwidth', 10) self.cnf['mousewheel'] = kw.pop('mousewheel', True) self.cnf['avoidmousewheel'] = kw.pop('avoidmousewheel', ()) self.cnf['canvas'] = kw.pop('canvas', _tk.Canvas(master=master, highlightthickness=0, width=kw.pop('width', 250), height=kw.pop('height', 250))) self.cnf['scrollbar'] = kw.pop('scrollbar', _tk.Scrollbar(self.cnf['canvas'], orient='vertical', width=self.cnf['scrollbarwidth'])) _tk.Frame.__init__(self, self.cnf['canvas'], **kw) self.cnf['canvas']['bg'] = self['bg'] self.cnf['scrollbar'].place(relx=1, rely=0, anchor='ne', relheight=1) self.cnf['scrollbar'].configure(command=self.cnf['canvas'].yview) self.cnf['canvas'].configure(yscrollcommand=self.cnf['scrollbar'].set) self.cnf['canvas'].create_window(0, 0, anchor='nw', tags="window", window=self, width=self.cnf['canvas'].winfo_reqwidth()-\ self.cnf['scrollbar'].winfo_reqwidth()) self.cnf['canvas'].bind("", self._configure_height, add="+") _bind(self, className='configure', sequence='', func=self._configure_window) self._mouse_scrolling(self.cnf['mousewheel']) self._avoid_mousewheel(self.cnf.get('avoidmousewheel')) self._geometryManager() def _avoid_mousewheel(self, widgets): """Internal function.\n Use this to have multiple scrollable widgets inside of SFrame.""" def set_widget(wid): """Internal function.\n Binds and to the widget to enable/disable mousewheel scrolling.""" binds = [{'className': 'mw_state_sframe', 'sequence': '', 'func': lambda _: self._mouse_scrolling(True)}] if not isinstance(wid, SFrameBase): binds.append({'className': 'mw_state_sframe', 'sequence': '', 'func': lambda _: self._mouse_scrolling(False)}) _bind(wid, *binds) if isinstance(widgets, (list, tuple)): for widget in widgets: set_widget(widget) else: set_widget(widgets) def _mouse_scrolling(self, state): """Internal function.""" def enable_mousewheel(evt=None): """Internal function.""" self.bind_all('', self._on_mouse_scroll) def disable_mousewheel(evt=None): """Internal function.""" self.unbind_all('') if state: _bind(self, {'className': 'mousewheel_state', 'sequence': '', 'func': enable_mousewheel}, {'className': 'mousewheel_state', 'sequence': '', 'func': disable_mousewheel}) enable_mousewheel() else: _bind(self, {'className': 'mousewheel_state', 'sequence': ''}, {'className': 'mousewheel_state', 'sequence': ''}) disable_mousewheel() def _on_mouse_scroll(self, evt): """Internal function.""" if self.winfo_height() < self.cnf['canvas'].winfo_height(): return if evt.state == 0: self.cnf['canvas'].yview_scroll(-1*delta(evt), 'units') def _configure_height(self, evt): """Internal function.""" width = self.cnf['canvas'].winfo_width()-self.cnf['scrollbar'].winfo_width() self.cnf['canvas'].itemconfig('window', width=width) def _configure_window(self, evt): """Internal function.""" # this will update the position of scrollbar when scrolled from mousewheel. # fixes some bugs # makes scrolling more smoother self.after_cancel(self._after_ids.get(0, ' ')) self._after_ids[0] = self.after(1, lambda: self.cnf['canvas'].configure( scrollregion=self.cnf['canvas'].bbox('all'))) def _geometryManager(self): """Internal function.""" # Use set to support the following in both python 2 and python 3 geo_methods = [m for m in (set(_tk.Pack.__dict__) | set(_tk.Grid.__dict__) | set(_tk.Place.__dict__)) if m not in _tk.Frame.__dict__] for m in geo_methods: if m[0] != '_' and 'config' not in m: setattr(self, m, getattr(self.cnf['canvas'], m)) def _configure(self, cmd, cnf=None, kw=None): kw = _tk._cnfmerge((cnf, kw)) self.cnf['scrollbar']['width'] = kw.pop( 'scrollbarwidth', self.cnf['scrollbar']['width']) for key in kw.copy(): if key in self._features: self.cnf[key] = kw.pop(key, self.cnf.get(key)) self.cnf['canvas']['width'] = kw.pop( 'width', self.cnf['canvas']['width']) self.cnf['canvas']['height'] = kw.pop( 'height', self.cnf['canvas']['height']) self._mouse_scrolling(self.cnf['mousewheel']) self._avoid_mousewheel(self.cnf['avoidmousewheel']) _return = _tk.Frame._configure(self, cmd, {}, kw) if kw.get('bg', kw.get('background')): self.cnf['canvas']['bg'] = self['bg'] if isinstance(_return, dict): _return.update(self.cnf) return _return def cget(self, key): """Return the resource value for a KEY given as string.""" if key in self._features: return self.cnf.get(key) return _tk.Frame.cget(self, key) __getitem__ = cget class MarqueeBase(_tk.Canvas): """Base class for Marquee.""" def __init__(self, master=None, cnf={}, **kw): kw = _cnfmerge((cnf, kw)) self._stop_state = False self.cnf = dict( text=kw.pop('text', ''), font=kw.pop('font', None), fg=kw.pop('fg', 'black') if kw.get( 'fg') else kw.pop('foreground', 'black'), fps=kw.pop('fps', 30), left_margin=kw.pop('left_margin', 10), initial_delay=kw.pop('initial_delay', 1000), end_delay=kw.pop('end_delay', 1000), smoothness=kw.pop('smoothness', 1), # 1 <= smooth < 1 ) kw['height'] = kw.get('height', 24) kw['highlightthickness'] = kw.get('highlightthickness', 0) _tk.Canvas.__init__(self, master=master, **kw) self._create('text', (3, 1), dict(anchor='w', tag='text', text=self.cnf.get('text'), font=self.cnf.get('font'), fill=self.cnf.get('fg'))) _bind(self, className='configure', sequence='', func=self._check) self.after_id = ' ' def _set_height(self, evt=None): """Internal function.""" bbox = self.bbox('text') height = bbox[3] - bbox[1] + 8 if int(self['height']) == height: return _tk.Canvas._configure(self, 'configure', {'height': height}, None) def _reset(self, force_reset=False): """Internal function.\n Resets the text position, do not call directly.""" if self.after_id == ' ' and not force_reset: return self.after_cancel(self.after_id) self.coords('text', 3, self.winfo_height()/2) # RESETS TEXT self.after_id = ' ' def _check(self, evt=None): """Internal function.\n Sets the text properly in the frame.""" self._set_height() self.coords('text', 3, self.winfo_height()/2) text_width = self.bbox('text')[2] # TEXT WIDTH frame_width = self.winfo_width() # FRAME WIDTH if text_width + 1 < frame_width: self._reset() elif self.after_id == ' ': delay = self.cnf.get('initial_delay') # INITITAL DEALY self.after_id = self.after(delay, self._animate) def _animate(self, evt=None): """Internal function.\n Process text and move text.""" if self._stop_state: return self._set_height() text_width = self.bbox('text')[2] # TEXT WIDTH frame_width = self.winfo_width() # FRAME WIDTH delay = int(self.cnf.get('smoothness')*1000 / self.cnf.get('fps')) if text_width + 1 + self.cnf.get('left_margin') < frame_width: self.after(self.cnf.get('end_delay'), self.coords, 'text', 3, self.winfo_height()/2) # RESETS TEXT delay = self.cnf.get('initial_delay') + \ self.cnf.get('end_delay') # INITITAL DEALY else: # MOVE -1 PIXEL EVERYTIME self.tk.call((self._w, 'move') + ( 'text', -self.cnf.get('smoothness'), 0)) self.after_id = self.after(delay, self._animate) def _configure(self, cmd, cnf=None, kw=None): """Internal function.""" kw = _cnfmerge((cnf, kw)) self.cnf = dict( text=kw.pop('text', self.cnf.get('text')), font=kw.pop('font', self.cnf.get('font')), fg=kw.pop('fg', self.cnf.get('fg')) if kw.get('fg') else kw.pop('foreground', self.cnf.get('foreground')), fps=kw.pop('fps', self.cnf.get('fps')), left_margin=kw.pop('left_margin', self.cnf.get('left_margin')), initial_delay=kw.pop( 'initial_delay', self.cnf.get('initial_delay')), end_delay=kw.pop('end_delay', self.cnf.get('end_delay')), smoothness=kw.pop('smoothness', self.cnf.get('smoothness')), ) _tk.Canvas._configure(self, ('itemconfigure','text'), dict(text=self.cnf.get('text'), font=self.cnf.get('font'), fill=self.cnf.get('fg')), None) self._set_height() return _tk.Canvas._configure(self, cmd, kw, None) def cget(self, key): """Return the resource value for a KEY given as string.""" if key in self.cnf.keys(): return self.cnf[key] return _tk.Canvas.cget(self, key) __getitem__ = cget def destroy(self): """Destroy this widget.""" self.after_cancel(self.after_id) return _tk.Canvas.destroy(self)