diff --git a/generated/3.0/examples/cocoavlc.py b/generated/3.0/examples/cocoavlc.py index caf2266..cd61e09 100755 --- a/generated/3.0/examples/cocoavlc.py +++ b/generated/3.0/examples/cocoavlc.py @@ -46,7 +46,7 @@ def _PyPI(package): return 'see ' % (package,) __all__ = ('AppVLC',) # PYCHOK expected -__version__ = '21.11.02' +__version__ = '22.12.14' try: import vlc @@ -57,9 +57,9 @@ def _PyPI(package): __version__ as _pycocoa_version except ImportError: raise ImportError('no %s, %s' % (_pycocoa_, _PyPI('PyCocoa'))) -if _pycocoa_version < __version__: +if _pycocoa_version < '21.11.04': # __version__ raise ImportError('%s %s or later required, %s' % ( - _pycocoa_, __version__, _PyPI('PyCocoa'))) + _pycocoa_, '21.11.04', _PyPI('PyCocoa'))) del _PyPI # all imports listed explicitly to help PyChecker @@ -68,7 +68,7 @@ def _PyPI(package): OpenPanel, printf, str2bytes, Table, z1000str, zSIstr from os.path import basename, getsize, isfile, splitext -import platform as _platform +from platform import architecture, mac_ver import sys from threading import Thread from time import sleep, strftime, strptime @@ -84,9 +84,11 @@ def _PyPI(package): _Adjust.Gamma: (0.01, 1, 10), _Adjust.Hue: (-180, 0, 180), _Adjust.Saturation: (0, 1, 3)} +_AppleSi = machine().startswith('arm64') _Argv0 = splitext(basename(__file__))[0] _Movies = '.m4v', '.mov', '.mp4' # lower-case file types for movies, videos -_Python = sys.version.split()[0], _platform.architecture()[0] # PYCHOK false +_PNG = '.png' # snapshot always .png, even if .jpg or .tiff specified +_Python = sys.version.split()[0], architecture()[0] # PYCHOK false _Select = 'Select a video file from the panel' _VLC_3_ = vlc.__version__.split('.')[0] > '2' and \ bytes2str(vlc.libvlc_get_version().split(b'.')[0]) > '2' @@ -114,9 +116,28 @@ class _Color(object): # PYCHOK expected _Color = _Color() # PYCHOK enum-like +def _fstrz(f, n=1, x=''): + # format float, strip trailing decimal zeros and point + return _fstrz0(f, n).rstrip('.') + x + + +def _fstrz0(f, n=1, x=''): + # format float, strip trailing decimal zeros + t = '%.*f' % (n, f) + return t.rstrip('0') + x + + +def _fstrz1(f, n=1, x=''): + # format float, strip trailing decimal zeros, except one + t = _fstrz0(f, n) + if t.endswith('.'): + t += '0' + return t + x + + def _macOS(sep=None): # get macOS version and extended platform.machine - t = 'macOS', _platform.mac_ver()[0], machine() + t = 'macOS', mac_ver()[0], machine() return sep.join(t) if sep else t @@ -127,7 +148,7 @@ def _mspf(fps): def _ms2str(ms): # convert milliseconds to seconds string - return '%.3f s' % (max(ms, 0) * 0.001,) + return _fstrz1(max(ms, 0) * 0.001, 3, ' s') def _ratio2str(by, *w_h): @@ -144,39 +165,43 @@ class AppVLC(App): ''' adjustr = '' marquee = None + media = None logostr = '' player = None raiser = False - scale = 1 # media zoom factor - sized = None # video width, height + rate = 0.0 # rate vs normal + scale = 0.0 # video size / window size + sized = None # video (width, height) Snapshot = Item('Snapshot', key='s', alt=True) - snapshot = '.png' # default, or .jpg or .tiff + snapshot = _PNG # default: .png, .jpg or .tiff snapshots = 0 Toggle = None video = None window = None + zoomX = 1.0 # zoom factor, >= 1.0 def __init__(self, video=None, # video file name adjustr='', # vlc.VideoAdjustOption logostr='', # vlc.VideoLogoOption marquee=False, # vlc.VideoMarqueeOption raiser=False, # re-raise errors - snapshot='png', # png, jpg or tiff format + snapshot=_PNG, # png, other formats title='AppVLC'): # window title super(AppVLC, self).__init__(raiser=raiser, title=title) - self.adjustr = adjustr - self.logostr = logostr - self.marquee = marquee - self.media = None - self.raiser = raiser - self.snapshot = '.' + snapshot.lstrip('.').lower() - self.Toggle = Item('Play', self.menuToggle_, key='p', ctrl=True) - self.video = video - - if self.snapshot == '.png': + self.adjustr = adjustr + self.logostr = logostr + self.marquee = marquee +# self.media = None + self.raiser = raiser + self.Toggle = Item('Play', self.menuToggle_, key='p', ctrl=True) + self.video = video + + if snapshot != AppVLC.snapshot: + self.snapshot = '.' + snapshot.lstrip('.').lower() + if self.snapshot in (_PNG,): # only .PNG works, using .JPG ... + # ... or .TIFF is OK, but the snapshot image is always .PNG self.player = vlc.MediaPlayer() -# # XXX does not work, snapshots are always png -# elif self.snapshot in ('.jpg', '.tiff'): +# elif self.snapshot in (_JPG, _PNG, _TIFF): # XXX doesn't work # i = vlc.Instance('--snapshot-format', self.snapshot[1:]) # --verbose 2 # self.player = i.media_player_new() else: @@ -217,26 +242,28 @@ def appLaunched_(self, app): # see function pycocoa.title2action. Item('Open...', key='o'), ItemSeparator(), - Item('Info', key='i'), - Item('Close Windows', key='w'), - ItemSeparator(), self.Toggle, # Play >< Pause Item('Rewind', key='r', ctrl=True), ItemSeparator(), - Item('Zoom In', key='+'), + Item('Info', key='i'), + Item('Close', key='w'), + ItemSeparator(), + Item('Zoom In', key='+', shift=True), Item('Zoom Out', key='-'), ItemSeparator(), Item('Faster', key='>', shift=True), Item('Slower', key='<', shift=True)) if _VLC_3_: menu.append( + ItemSeparator(), Item('Brighter', key='b', shift=True), Item('Darker', key='d', shift=True)) menu.append( + ItemSeparator(), + Item('Normal 1X', key='='), ItemSeparator(), Item('Audio Filters', self.menuFilters_, key='a', shift=True), - Item('Video Filters', self.menuFilters_, key='v', shift=True)) - menu.append( + Item('Video Filters', self.menuFilters_, key='v', shift=True), ItemSeparator(), self.Snapshot) self.append(menu) @@ -247,7 +274,7 @@ def appLaunched_(self, app): def menuBrighter_(self, item): self._brightness(item, +0.1) - def menuCloseWindows_(self, item): # PYCHOK expected + def menuClose_(self, item): # PYCHOK expected # close window(s) from menu Cmd+W # printf('%s %r', 'close_', item) if not closeTables(): @@ -310,36 +337,37 @@ def menuInfo_(self, item): t.append('size', z1000str(z), zSIstr(z)) t.append('state', str(p.get_state())) f = max(p.get_position(), 0) - t.append('position/length', '%.2f%%' % (f * 100,), _ms2str(p.get_length())) + t.append('position/length', _fstrz(f * 100, 2), _ms2str(p.get_length())) f = map(_ms2str, (p.get_time(), m.get_duration())) t.append('time/duration', *f) t.append('track/count', z1000str(p.video_get_track()), z1000str(p.video_get_track_count())) t.separator() f = p.get_fps() - t.append('fps/mspf', '%.6f' % (f,), '%.3f ms' % (_mspf(f),)) + t.append('fps/mspf', _fstrz(f, 5), _fstrz(_mspf(f), 3, ' ms')) r = p.get_rate() - t.append('rate', '%s%%' % (int(r * 100),), r) - w, h = p.video_get_size(0) - t.append('video size', _ratio2str('x', w, h)) # num=0 - r = _ratio2str(':', *aspect_ratio(w, h)) # p.video_get_aspect_ratio() + t.append('rate', r, '%s%%' % (int(r * 100),)) + a, b = p.video_get_size(0) # num=0 + w, h = map(int, self.window.frame.size.size) + t.append('video size', _ratio2str('x', a, b), _ratio2str('x', w, h)) + r = _ratio2str(':', *aspect_ratio(a, b)) # p.video_get_aspect_ratio() t.append('aspect ratio', r, _ratio2str(':', *self.window.ratio)) - t.append('scale', '%.3f' % (p.video_get_scale(),), '%.3f' % (self.scale,)) + t.append('scale', _fstrz1(p.video_get_scale(), 3), _fstrz(self.zoomX, 2, 'X')) t.separator() - def VLCadjustr2(option): # get option value + def VLCadjustr3(f, option): # get option value lo, _, hi = _Adjust3[option] - f = self.player.video_get_adjust_float(option) - p = max(0, (f - lo)) * 100.0 / (hi - lo) - t = '%.2f %.1f%%' % (f, p) - # return 2-tuple (value, percentage) as strings - return t.replace('.0%', '%').replace('.00', '.0').split() - - t.append('brightness', *VLCadjustr2(_Adjust.Brightness)) - t.append('contrast', *VLCadjustr2(_Adjust.Contrast)) - t.append('gamma', *VLCadjustr2(_Adjust.Gamma)) - t.append('hue', *VLCadjustr2(_Adjust.Hue)) - t.append('saturation', *VLCadjustr2(_Adjust.Saturation)) + v = f(option) + p = max(0, (v - lo)) * 100.0 / (hi - lo) + n = str(option).split('.')[-1] # 'VideoAdjustOption.Xyz' + return n.lower(), _fstrz1(v, 2), _fstrz(p, 1, '%') + + f = self.player.video_get_adjust_float + t.append(*VLCadjustr3(f, _Adjust.Brightness)) + t.append(*VLCadjustr3(f, _Adjust.Contrast)) + t.append(*VLCadjustr3(f, _Adjust.Gamma)) + t.append(*VLCadjustr3(f, _Adjust.Hue)) + t.append(*VLCadjustr3(f, _Adjust.Saturation)) t.separator() s = vlc.MediaStats() # re-use single MediaStats instance? @@ -373,6 +401,16 @@ def Kops2bpstr2(bitrate): # convert Ko/s to bits/sec raise printf('%s', x, nl=1, nt=1) + def menuNormal1X_(self, item): + # set rate and zoom to 1X + self._brightness(item) +# self._contrast(item) +# self._gamma(item) +# self._hue(item) + self._rate(item) +# self._saturation(item) + self._zoom(item) + def menuOpen_(self, item): # stop the current video and show # the panel to select another video @@ -382,7 +420,7 @@ def menuOpen_(self, item): if v: self.window.title = self.video = v self.player.set_mrl(v) - self.sized = None + self._reset() def menuPause_(self, item, pause=False): # PYCHOK expected # note, .player.pause() pauses and un-pauses the video, @@ -400,10 +438,11 @@ def menuPlay_(self, item_or_None): # PYCHOK expected def menuRewind_(self, item): # PYCHOK expected self.player.set_position(0.0) + self.player.set_time(0.0) # note, can't re-play once at the end # self.menuPlay_() self.badge.label = 'R' - self.sized = None + self._reset() def menuSlower_(self, item): self._rate(item, 0.80) @@ -446,32 +485,38 @@ def windowLast_(self, window): def windowResize_(self, window): if window is self.window: - self._resizer() + self._reset(True) super(AppVLC, self).windowResize_(window) def windowScreen_(self, window, change): if window is self.window: - self._resizer() + self._reset(True) super(AppVLC, self).windowScreen_(window, change) - def _brightness(self, unused, fraction): # change brightness + def _brightness(self, unused, fraction=0): # change brightness self._VLCadjust(_Adjust.Brightness, fraction) - def _contrast(self, unused, fraction): # change contrast + def _contrast(self, unused, fraction=0): # change contrast self._VLCadjust(_Adjust.Contrast, fraction) - def _gamma(self, unused, fraction): # change gamma + def _gamma(self, unused, fraction=0): # change gamma self._VLCadjust(_Adjust.Gamma, fraction) - def _hue(self, unused, fraction): # change hue + def _hue(self, unused, fraction=0): # change hue self._VLCadjust(_Adjust.Hue, fraction) - def _saturation(self, unused, fraction): # change saturation - self._VLCadjust(_Adjust.Saturation, fraction) + def _rate(self, unused, factor=0): # change the video rate + p = self.player + r = p.get_rate() * factor + r = max(0.2, min(10.0, r)) if r > 0 else 1.0 + p.set_rate(r) + self.rate = r - def _rate(self, unused, factor): # change the video rate - r = max(0.2, min(10.0, self.player.get_rate() * factor)) - self.player.set_rate(r) + def _reset(self, resize=False): + self.zoomX = 1 + self.sized = None + if resize: + Thread(target=self._sizer).start() def _resizer(self): # adjust aspect ratio and marquee height if self.sized: @@ -480,19 +525,21 @@ def _resizer(self): # adjust aspect ratio and marquee height else: Thread(target=self._sizer).start() - def _sizer(self, secs=0.25): + def _saturation(self, unused, fraction=0): # change saturation + self._VLCadjust(_Adjust.Saturation, fraction) + + def _sizer(self, secs=0.25): # asynchronously while True: - p = self.player - # wiggle the video to fill the window - s = p.video_get_scale() - p.video_set_scale(0.0 if s else 1.0) - p.video_set_scale(s) # the first call(s) returns (0, 0), # subsequent calls return (w, h) - w, h = p.video_get_size(0) - if h > 0 and w > 0: - # window's contents' aspect ratio - self.window.ratio = self.sized = w, h + a, b = self.player.video_get_size(0) + if b > 0 and a > 0: + w = self.window + # set window's contents' aspect ratio + w.ratio = self.sized = a, b + # get video scale factor + self.scale = float(w.frame.width) / a + self._wiggle() break elif secs > 0.001: sleep(secs) @@ -506,12 +553,12 @@ def _VLCadjust(self, option, fraction=0, value=None): # note, .Enable must be set to 1, but once is sufficient p.video_set_adjust_int(_Adjust.Enable, 1) try: - lo, _, hi = _Adjust3[option] - if value is None: - v = p.video_get_adjust_float(option) - else: - v = float(value) + lo, v, hi = _Adjust3[option] if fraction: + if value is None: + v = p.video_get_adjust_float(option) + else: + v = float(value) v += fraction * (hi - lo) v = float(max(lo, min(hi, v))) p.video_set_adjust_float(option, v) @@ -545,11 +592,24 @@ def _VLCmarquee(self, size=36): p.video_set_marquee_int(m.Refresh, 1000) # millisec (or sec?) p.video_set_marquee_string(m.Text, str2bytes('%Y-%m-%d %T %z')) - def _zoom(self, unused, factor): - # zoom the video rate in/out - if factor > 0: - self.scale *= factor - self.player.video_set_scale(self.scale) # if abs(self.scale - 1.0) > 0.01 else 0.0) + def _wiggle(self): + # wiggle the video to fill the window + p = self.player + s = p.video_get_scale() + p.video_set_scale(0.0 if s else self.scale) + p.video_set_scale(s) + + def _zoom(self, unused, factor=0): + # zoom the video in/out, see tkvlc.py + p = self.player + x = self.zoomX * factor + if x > 1: + s = x + else: # not below 1X + s, x = 0.0, 1.0 + p.video_set_scale(s) + self.scale = s + self.zoomX = x if __name__ == '__main__': # MCCABE 24 @@ -570,7 +630,7 @@ def _Adjustr(): _logostr = '' _marquee = False _raiser = False - _snapshot = 'png' + _snapshot = AppVLC.snapshot # default _timeout = None _title = splitext(_argv0)[0] _video = None diff --git a/generated/3.0/examples/gtkvlc.py b/generated/3.0/examples/gtkvlc.py index a326e68..48baa61 100755 --- a/generated/3.0/examples/gtkvlc.py +++ b/generated/3.0/examples/gtkvlc.py @@ -121,7 +121,7 @@ def get_player_control_toolbar(self): (_("Quit"), _("Quit"), 'gtk-quit', Gtk.main_quit), ): i = Gtk.Image.new_from_icon_name(iconname, Gtk.IconSize.MENU) - b = Gtk.ToolButton(i, text) + b = Gtk.ToolButton.new(i, text) b.set_tooltip_text(tooltip) b.connect("clicked", callback) tb.insert(b, -1) diff --git a/generated/3.0/examples/tkvlc.py b/generated/3.0/examples/tkvlc.py index bef1606..47a9151 100755 --- a/generated/3.0/examples/tkvlc.py +++ b/generated/3.0/examples/tkvlc.py @@ -26,29 +26,82 @@ # Tested with VLC 3.0.16, 3.0.12, 3.0.11, 3.0.10, 3.0.8 and 3.0.6 with # the compatible vlc.py Python-VLC binding, Python 3.11.0, 3.10.0, 3.9.0 -# and 3.7.4 with tkinter/Tk 8.6.9 on macOS 13.0 (amd64 M1), 11.6.1 (10.16 -# amd64 M1), 11.0.1 (10.16 x86-64) and 10.13.6 only. -__version__ = '22.11.11' # mrJean1 at Gmail +# and 3.7.4 and with tkinter/Tk 8.6.9 on macOS 13.0.1 (amd64 M1), 11.6.1 +# (10.16 amd64 M1), 11.0.1 (10.16 x86-64) and 10.13.6 and with VLC 3.0.18, +# Python 3.11.0 and tkinter/Tk 8.6.9 on Windows 10, all in 64-bit only. +__version__ = '22.12.28' # mrJean1 at Gmail import sys try: # Python 3.4+ only import tkinter as Tk - from tkinter import ttk # PYCHOK ttk = Tk.ttk + from tkinter import TclError, ttk # PYCHOK ttk = Tk.ttk from tkinter.filedialog import askopenfilename from tkinter.messagebox import showerror - from pathlib import Path except ImportError: sys.exit('%s requires Python 3.4 or later' % (sys.argv[0],)) - # import Tkinter as Tk + # import Tkinter as Tk; ttk = Tk import os import time import vlc -_dragging = False # use -dragging option - _isMacOS = sys.platform.startswith('darwin') -_isWindows = sys.platform.startswith('win') _isLinux = sys.platform.startswith('linux') +_isWindows = sys.platform.startswith('win') + +_ANCHORED = 'Anchored' +_BANNER_H = 32 if _isMacOS else 64 +_BUTTONS = 'Buttons' +_DISABLED = ( Tk.DISABLED,) +_ENABLED = ('!' + Tk.DISABLED,) +_FULL_OFF = 'Full Off' +_FULL_SCREEN = 'Full Screen' +# see _Tk_Menu.add_item and .bind_shortcut below +# +_KEY_SYMBOL = {'~': 'asciitilde', '`': 'grave', + '!': 'exclam', '@': 'at', + '#': 'numbersign', '$': 'dollar', + '%': 'percent', '^': 'asciicirum', + '&': 'ampersand', '*': 'asterisk', + '(': 'parenleft', ')': 'parenright', + '_': 'underscore', '-': 'minus', + '+': 'plus', '=': 'equal', + '{': 'braceleft', '}': 'braceright', + '[': 'bracketleft', ']': 'bracketright', + '|': 'bar', '\\': 'backslash', + ':': 'colon', ';': 'semicolon', + '"': 'quotedbl', "'": 'apostrophe', + '<': 'less', '>': 'greater', + ',': 'comma', '.': 'period', + '?': 'question', '/': 'slash', + ' ': 'space', '\b': 'BackSpace', # S! + '\n': 'KP_Enter', '\r': 'Return', + '\f': 'Next', '\v': 'Prior', + '\t': 'Tab'} # '\a': 'space'? +# see definition of specialAccelerators in +_MAC_ACCEL = {' ': 'Space', '\b': 'Backspace', # s! + '\n': 'Enter', '\r': 'Return', + '\f': 'PageDown', '\v': 'PageUp', + '\t': 'Tab', # 'BackTab', 'Eject'? + 'Prior': 'PageUp', 'Next': 'PageDown'} + +_MIN_W = 420 +_MOD_ALT = 1 << 17 # alt key down? +_MOD_CMD = 1 << 3 # command key down +_MOD_CTRL = 1 << 2 # ctrl key down +_MOD_LOCK = 1 << 1 # caps lock down +_MOD_SHIFT = 1 << 0 # shift key down +_OPACITY = 'Opacity %s%%' +_TAB_X = 32 +_T_CONFIGURE = Tk.EventType.Configure +_T_KEY = Tk.EventType.Key # KeyPress +_TICK_MS = 100 # millisecs per time tick +_Tk_Canvas = Tk.Canvas +_Tk_Frame = ttk.Frame +_Tk_Toplevel = Tk.Toplevel +_Tk_Version = Tk.TkVersion +_UN_ANCHORED = 'Un-' + _ANCHORED +_VOLUME = 'Volume' _TKVLC_LIBTK_PATH = 'TKVLC_LIBTK_PATH' @@ -62,11 +115,12 @@ # Python 3+ or one matching the version of tkinter # Homebrew-built Python, Tcl/Tk, etc. are installed in - # different places, usually something like /usr/- or + # different places, usually something like (/usr/- or) # /opt/local/Cellar/tcl-tk/8.6.11_1/lib/libtk8.6.dylib, # found by command line `find /opt -name libtk8.6.dylib` def _find_lib(name, *paths): + assert os.path.sep == '/' # 1. built into Python for p in (getattr(sys, 'base_prefix', ''), sys.prefix): if p: @@ -78,17 +132,16 @@ def _find_lib(name, *paths): yield p if not p.endswith(name): yield p + '/' + name - # 3. try Homebrew basement + # 3. the Homebrew basement from glob import iglob for t in ('/opt', '/usr'): t += '/local/Cellar/tcl-tk/*/lib/' + name for p in iglob(t): yield p - assert os.path.sep == '/' try: env = os.environ.get(_TKVLC_LIBTK_PATH, '') - lib = 'libtk%s.dylib' % (Tk.TkVersion,) + lib = 'libtk%s.dylib' % (_Tk_Version,) for libtk in _find_lib(lib, _find(lib), *env.split(os.pathsep)): if libtk and lib in libtk and os.access(libtk, os.F_OK): break @@ -100,543 +153,1133 @@ def _find_lib(name, *paths): raise NameError(t % (_TKVLC_LIBTK_PATH,)) lib = cdll.LoadLibrary(libtk) - # getNSView = tklib.TkMacOSXDrawableView is the + # _GetNSView = lib.TkMacOSXDrawableView is the # proper function to call, but that is non-public # (in Tk source file macosx/TkMacOSXSubwindows.c) - # Fortunately, tklib.TkMacOSXGetRootControl calls - # tklib.TkMacOSXDrawableView and returns the NSView + # Fortunately, lib.TkMacOSXGetRootControl calls + # lib.TkMacOSXDrawableView and returns the NSView _GetNSView = lib.TkMacOSXGetRootControl # C signature: void *_GetNSView(void *drawable) to get # the Cocoa/Obj-C NSWindow.contentView attribute, the # drawable NSView object of the (drawable) NSWindow - _GetNSView.restype = c_void_p + _GetNSView.restype = c_void_p _GetNSView.argtypes = (c_void_p,) except (NameError, OSError) as x: # lib, image or symbol not found - libtk = str(x) # imported by psgvlc.py + libtk = str(x) # imported by examples/psgvlc.py - def _GetNSView(unused): # imported by psgvlc.py + def _GetNSView(unused): # imported by examples/psgvlc.py return None - del cdll, c_void_p, env, _find - C_Key = 'Command-' # shortcut key modifier + del cdll, c_void_p, env, _find, lib + Cmd_ = 'Command+' # bind key modifier, aka Meta_L + # With Python 3.9+ on macOS (only!), accelerator keys specified + # with the Shift modifier invoke the callback (command) twice, + # once without and once with a Key (or KeyPress) event: hold the + # former as a pseudo Key event possibly absorbed by the actual + # Key event about 1 millisec later. With Python 3.8- on macOS, + # Shift accelerator keys do not work at all: do not define any + # Shift accelerator keys in that case + _3_9 = sys.version_info[:2] >= (3, 9) + +else: # Windows OK, untested on *nix, Xwindows + libtk = 'N/A' + Cmd_ = 'Ctrl+' # bind key modifier: Control! + _3_9 = True + + +def _fullscreen(panel, *full): + # get/set a panel full-screen or -off + f = panel.attributes('-fullscreen') # or .wm_attributes + if full: + panel.attributes('-fullscreen', bool(full[0])) + panel.update_idletasks() + return f + + +def _geometry(panel, g_w, *h_x_y): + # set a panel geometry to C{g} or C{w, h, x, y}. + if h_x_y: + t = '+'.join(map(str, h_x_y)) + g = 'x'.join((str(g_w), t)) + else: + g = g_w + panel.geometry(g) # update geometry, then ... + g, *t = _geometry5(panel) # ... get actual ... + panel._g = g # ... as a C{str} and 4 C{int}s + # == panel.winfo_width(), _height(), _x(), _y() + panel._whxy = tuple(map(int, t)) + return g + + +def _geometry1(panel): + # get a panel geometry as C{str} + panel.update_idletasks() + return panel.geometry() + + +def _geometry5(panel): + # get a panel geometry as 5-tuple of C{str}s + g = _geometry1(panel) # '+-x' means absolute -x + z, x, y = g.split('+') + w, h = z.split('x') + return g, w, h, x, y + + +def _hms(tensecs, secs=''): + # format a time (in 1/10-secs) as h:mm:ss.s + s = tensecs * 0.1 + if s < 60: + t = '%3.1f%s' % (s, secs) + else: + m, s = divmod(s, 60) + if m < 60: + t = '%d:%04.1f' % (int(m), s) + else: + h, m = divmod(m, 60) + t = '%d:%02d:%04.1f' % (int(h), int(m), s) + return t -else: # *nix, Xwindows and Windows, UNTESTED - libtk = 'N/A' - C_Key = 'Control-' # shortcut key modifier +def _underline2(c, label='', underline=-1, **cfg): + # update cfg with C{underline=index} or remove C{underline=.} + u = label.find(c) if c and label else underline + if u >= 0: + cfg.update(underline=u) + else: # no underlining + c = '' + cfg.update(label=label) + return c, cfg + + +class _Tk_Button(ttk.Button): + '''A C{_Tk_Button} with a label, inlieu of text. + ''' + def __init__(self, frame, **kwds): + cfg = self._cfg(**kwds) + ttk.Button.__init__(self, frame, **cfg) + + def _cfg(self, label=None, **kwds): + if label is None: + cfg = kwds + else: + cfg = dict(text=label) + cfg.update(kwds) + return cfg + + def config(self, **kwds): + cfg = self._cfg(**kwds) + ttk.Button.config(self, **cfg) + + def disabled(self, *disable): + '''Dis-/enable this button. + ''' + # + p = self.instate(_DISABLED) + if disable: + self.state(_DISABLED if disable[0] else _ENABLED) + return bool(p) + + +class _Tk_Item(object): + '''A re-configurable C{_Tk_Menu} item. + ''' + def __init__(self, menu, label='', key='', under='', **kwds): + '''New menu item. + ''' + self.menu = menu + self.idx = menu.index(label) + self.key = key # <...> + + self._cfg_d = dict(label=label, **kwds) + self._dis_d = False + self._under = under # lower case + + def config(self, **kwds): + '''Reconfigure this menu item. + ''' + cfg = self._cfg_d.copy() + cfg.update(kwds) + if self._under: # update underlining + _, cfg = _underline2(self._under, **cfg) + self.menu.entryconfig(self.idx, **cfg) + + def disabled(self, *disable): + '''Dis-/enable this menu item. + ''' + # + p = self._dis_d + if disable: + self._dis_d = d = bool(disable[0]) + self.config(state=Tk.DISABLED if d else Tk.NORMAL) + return p class _Tk_Menu(Tk.Menu): - '''Tk.Menu extended with .add_shortcut method. Note, this is a - kludge just to get Command-key shortcuts to work on macOS. - Modifiers like Ctrl-, Shift- and Option- are not handled! + '''C{Tk.Menu} extended with an C{.add_shortcut} method. + + Note, make C{Command-key} shortcuts on macOS work like + C{Control-key} shotcuts on X-/Windows using a *single* + character shortcut. + + Other modifiers like Shift- and Option- passed thru, + unmodified. ''' - _shortcuts_entries = {} - _shortcuts_widget = None + _shortcuts_entries = None # {}, see .bind_shortcuts_to + _shortcuts_widgets = () + + def __init__(self, master=None, **kwds): + # remove dashed line from X-/Windows tearoff menus + # like idlelib.editor.EditorWindow.createmenubar + # or use root.option_add('*tearOff', False) Off? + # as per + Tk.Menu.__init__(self, master, tearoff=False, **kwds) + + def add_item(self, label='', command=None, key='', **kwds): + '''C{Tk.menu.add_command} extended with shortcut key + accelerator, underline and binding and returning + a C{_Tk_Item} instance instead of an C{item} index. - def add_shortcut(self, label='', key='', command=None, **kwds): - '''Like Tk.menu.add_command extended with shortcut key. If needed use modifiers like Shift- and Alt_ or Option- - as before the shortcut key character. Do not include - the Command- or Control- modifier nor the <...> brackets - since those are handled here, depending on platform and - as needed for the binding. + before the *single* shortcut key character. Do NOT + include the Command- or Control- modifier, instead use + the platform-specific Cmd_, like Cmd_ + key. Also, + do NOT enclose the key in <...> brackets since those + are handled here as needed for the shortcut binding. ''' - # - if not key: - self.add_command(label=label, command=command, **kwds) - - elif _isMacOS: - # keys show as upper-case, always - self.add_command(label=label, accelerator=C_Key + key, - command=command, **kwds) - self.bind_shortcut(key, command, label) - - else: # XXX not tested, not tested, not tested - self.add_command(label=label, underline=label.lower().index(key), - command=command, **kwds) - self.bind_shortcut(key, command, label) - - def bind_shortcut(self, key, command, label=None): - '''Bind shortcut key, default modifier Command/Control. + assert callable(command), 'command=%r' % (command,) + return self._Item(Tk.Menu.add_command, key, label, + command=command, **kwds) + + def add_menu(self, label='', menu=None, key='', **kwds): # untested + '''C{Tk.menu.add_cascade} extended with shortcut key + accelerator, underline and binding and returning + a C{_Tk_Item} instance instead of an C{item} index. + ''' + assert isinstance(menu, _Tk_Menu), 'menu=%r' % (menu,) + return self._Item(Tk.Menu.add_cascade, key, label, + menu=menu, **kwds) + + def bind_shortcut(self, key='', command=None, label='', **unused): + '''Bind shortcut key "". ''' - # The accelerator modifiers on macOS are Command-, + # C{Accelerator} modifiers on macOS are Command-, # Ctrl-, Option- and Shift-, but for .bind[_all] use - # , , and , + # , , and + # with a shortcut key name or character (replaced + # with its _KEY_SYMBOL if non-alphanumeric) # - if self._shortcuts_widget: - if C_Key.lower() not in key.lower(): - key = '<%s%s>' % (C_Key, key.lstrip('<').rstrip('>')) - self._shortcuts_widget.bind(key, command) - # remember the shortcut key for this menu item - if label is not None: - item = self.index(label) - self._shortcuts_entries[item] = key - # The Tk modifier for macOS' Command key is called - # Meta, but there is only Meta_L[eft], no Meta_R[ight] - # and both keyboard command keys generate Meta_L events. - # Similarly for macOS' Option key, the modifier name is - # Alt and there's only Alt_L[eft], no Alt_R[ight] and - # both keyboard option keys generate Alt_L events. See: + # + if key and callable(command) and self._shortcuts_widgets: + for w in self._shortcuts_widgets: + w.bind(key, command) + if label: # remember the key in this menu + idx = self.index(label) + self._shortcuts_entries[idx] = key + # The Tk modifier for macOS' Command key is called Meta + # with Meta_L and Meta_R for the left and right keyboard + # keys. Similarly for macOS' Option key, the modifier + # name Alt with Alt_L and Alt_R. Previously, there were + # only the Meta_L and Alt_L keys/modifiers. See also # - def bind_shortcuts_to(self, widget): - '''Set the widget for the shortcut keys, usually root. + def bind_shortcuts_to(self, *widgets): + '''Set widget(s) to bind shortcut keys to, usually the + root and/or Toplevel widgets. ''' - self._shortcuts_widget = widget - - def entryconfig(self, item, **kwds): # PYCHOK signature - '''Update shortcut key binding if menu entry changed. - ''' - Tk.Menu.entryconfig(self, item, **kwds) - # adjust the shortcut key binding also - if self._shortcuts_widget: - key = self._shortcuts_entries.get(item, None) - if key is not None and 'command' in kwds: - self._shortcuts_widget.bind(key, kwds['command']) + self._shortcuts_entries = {} + self._shortcuts_widgets = widgets + def entryconfig(self, idx, command=None, **kwds): # PYCHOK signature + '''Update a menu item and the shortcut key binding + if the menu item command is being changed. -class Player(Tk.Frame): - '''The main window has to deal with events. + Note, C{idx} is the item's index in the menu, + see C{_Tk_Item} above. + ''' + if command is None: # XXX postcommand for sub-menu + Tk.Menu.entryconfig(self, idx, **kwds) + elif callable(command): # adjust the shortcut key binding + Tk.Menu.entryconfig(self, idx, command=command, **kwds) + key = self._shortcuts_entries.get(idx, None) + if key is not None: + for w in self._shortcuts_widgets: + w.bind(key, command) + + def _Item(self, add_, key, label, **kwds): + # Add and bind a menu item or sub~menu with an + # optional accelerator key (not <..> enclosed) + # or underline letter (preceded by underscore), + # see . + cfg = dict(label=label) + if key: # Use '+' sign, like key = "Ctrl+Shift+X" + if key.startswith('<') and key.endswith('>'): + c = '' # pass as-is, e.g. <> + else: + c = '+' # split into modifiers and char + if key.endswith(c): + m = key.rstrip(c).split(c) + else: + m = key.split(c) + c = m.pop() + for k in ('Key', 'KeyPress', 'KeyRelease'): + while k in m: + m.remove(k) + # adjust accelerator key for specials like KP_1, + # PageDown and PageUp (on macOS, see function + # ParseAccelerator in and definition + # of specialAccelerators in ) + a = _MAC_ACCEL.get(c, c) if _isMacOS else c + if a.upper().startswith('KP_'): + a = a[3:] + # accelerator strings are only used for display + # ('+' or '-' OK, ' ' space isn't for macOS) + cfg.update(accelerator='+'.join(m + [a])) + # replace key with Tk keysymb, allow F1 thru F35 + # (F19 on macOS) and because shortcut keys are + # case-sensitive, use lower-case unless specified + # as an upper-case letter with Shift+ modifier + s = _KEY_SYMBOL.get(c, c) + if len(s) == 1 and s.isupper() \ + and 'Shift' not in m: + s = s.lower() + # default to single key down case + if len(c) == 1: + m.append('Key') # == KeyPress + # replace Ctrl modifier with Tk Control + while 'Ctrl' in m: + m[m.index('Ctrl')] = 'Control' + # for .bind_shortcut/.bind + key = '<' + '-'.join(m + [s]) + '>' + if _isMacOS or len(c) != 1 or not c.isalnum(): + c = '' # no underlining + else: # only Windows? + c, cfg = _underline2(c, **cfg) + + else: # like idlelib, underline char after ... + c, u = '', label.find('_') # ... underscore + if u >= 0: # ... and remove underscore + label = label[:u] + label[u+1:] + cfg.update(label=label) + if u < len(label) and not _isMacOS: + # c = label[u] + cfg.update(underline=u) + + if kwds: # may still override accelerator ... + cfg.update(kwds) # ... and underline + add_(self, **cfg) # first _add then ... + self.bind_shortcut(key, **cfg) # ... bind + return _Tk_Item(self, key=key, under=c, **cfg) + + +class _Tk_Slider(Tk.Scale): + '''Scale with some add'l attributres ''' - _debugs = 0 - _geometry = '' - _MIN_WIDTH = 600 - _stopped = None + _var = None - def __init__(self, parent, title=None, video='', debug=False): # PYCHOK called! - Tk.Frame.__init__(self, parent) + def __init__(self, frame, to=1, **kwds): + if isinstance(to, int): + f, v = 0, Tk.IntVar() + else: + f, v = 0.0, Tk.DoubleVar() + cfg = dict(from_=f, to=to, + orient=Tk.HORIZONTAL, + showvalue=0, + variable=v) + cfg.update(kwds) + Tk.Scale.__init__(self, frame, **cfg) + self._var = v + + def set(self, value): + # doesn't move the slider + self._var.set(value) + Tk.Scale.set(self, value) + + +class Player(_Tk_Frame): + '''The main window handling with events, etc. + ''' + _anchored = True # under the video panel + _BUTTON_H = _BANNER_H + _debugs = 0 + _isFull = '' # or geometry + _length = 0 # length time ticks + _lengthstr = '' # length h:m:s + _muted = False + _opacity = 90 if _isMacOS else 100 # percent + _opaque = False + _rate = 0.0 + _ratestr = '' + _scaleX = 1 + _scaleXstr = '' + _sliding = False + _snapshots = 0 + _stopped = None + _title = 'tkVLCplayer' + _volume = 50 # percent + + def __init__(self, parent, title='', video='', debug=False): # PYCHOK called! + _Tk_Frame.__init__(self, parent) self.debug = bool(debug) self.parent = parent # == root - self.parent.title(title or 'tkVLCplayer') - self.video = os.path.expanduser(video) - - # Menu Bar - menubar = Tk.Menu(self.parent) - self.parent.config(menu=menubar) - # File Menu - fileMenu = _Tk_Menu(menubar) - fileMenu.bind_shortcuts_to(parent) # XXX must be root? - - fileMenu.add_shortcut('Open...', 'o', self.OnOpen) - fileMenu.add_separator() - fileMenu.add_shortcut('Play', 'p', self.OnPlay) # Play/Pause - fileMenu.add_command(label='Stop', command=self.OnStop) - fileMenu.add_separator() - fileMenu.add_shortcut('Mute', 'm', self.OnMute) - fileMenu.add_separator() - fileMenu.add_shortcut('Close', 'w' if _isMacOS else 's', self.OnClose) - fileMenu.add_separator() - fileMenu.add_shortcut('Buttons Up', 'a', self.OnAnchor) - self.anchorIndex = fileMenu.index('Buttons Up') - fileMenu.add_separator() - fileMenu.add_shortcut('Full Screen', 'f', self.OnScreen) - self.fullIndex = fileMenu.index('Full Screen') - menubar.add_cascade(label='File', menu=fileMenu) - self.fileMenu = fileMenu - self.playIndex = fileMenu.index('Play') - self.muteIndex = fileMenu.index('Mute') - - # first, panel shows video - self.videoPanel = ttk.Frame(self.parent) - self.canvas = Tk.Canvas(self.videoPanel) - self.canvas.pack(fill=Tk.BOTH, expand=1) - self.videoPanel.pack(fill=Tk.BOTH, expand=1) - - # panel to hold buttons - self.buttonsPanel = Tk.Toplevel(self.parent) - self.buttonsPanel.title('') - self.buttonsPanel_anchored = False - self.buttonsPanel_clicked = False - self.buttonsPanel_dragged = False - - buttons = ttk.Frame(self.buttonsPanel) - self.playButton = ttk.Button(buttons, text='Play', command=self.OnPlay, underline=0) - stop = ttk.Button(buttons, text='Stop', command=self.OnStop) - self.muteButton = ttk.Button(buttons, text='Mute', command=self.OnMute, underline=0) - self.playButton.pack(side=Tk.LEFT, padx=8) - stop.pack(side=Tk.LEFT) - self.muteButton.pack(side=Tk.LEFT, padx=8) - self.volMuted = False - self.volVar = Tk.IntVar() - self.volSlider = Tk.Scale(buttons, variable=self.volVar, command=self.OnVolume, - from_=0, to=100, orient=Tk.HORIZONTAL, length=170, - showvalue=0, label='Volume') - self.volSlider.pack(side=Tk.LEFT) - - self.anchorButton = ttk.Button(buttons, text='Up', command=self.OnAnchor, - width=2) # in characters - self.anchorButton.pack(side=Tk.RIGHT, padx=8) - buttons.pack(side=Tk.BOTTOM, fill=Tk.X) - - # - # - # self.buttonsPanel.attributes('-topmost', 1) - - self.buttonsPanel.update() - self.videoPanel.update() - - # panel to hold player time slider - timers = ttk.Frame(self.buttonsPanel) - self.timeVar = Tk.DoubleVar() - self.timeSliderLast = 0 - self.timeSlider = Tk.Scale(timers, variable=self.timeVar, command=self.OnTime, - from_=0, to=1000, orient=Tk.HORIZONTAL, length=500, - showvalue=0) # label='Time', - self.timeSlider.pack(side=Tk.BOTTOM, fill=Tk.X, expand=1) - self.timeSliderUpdate = time.time() - timers.pack(side=Tk.BOTTOM, fill=Tk.X) + self.video = os.path.expanduser(video) + if title: + self._title = str(title) + parent.title(self._title) +# parent.iconname(self._title) + + # set up tickers to avoid None error + def _pass(): + pass + + self._tick_a = self.after(1, _pass) + self._tick_c = self.after(2, _pass) + self._tick_r = self.after(3, _pass) + self._tick_s = self.after(4, _pass) # .after_idle + self._tick_t = self.after(5, _pass) + self._tick_z = self.after(6, _pass) + + # panels to play videos and hold buttons, sliders, + # created *before* the File menu to be able to bind + # the shortcuts keys to both windows/panels. + self.videoPanel = v = self._VideoPanel() + self._bind_events(v) # or parent + self.buttonsPanel = b = self._ButtonsPanel() + self._bind_events(b) + + mb = _Tk_Menu(self.parent) # menu bar + parent.config(menu=mb) +# self.menuBar = mb + + # macOS shortcuts + m = _Tk_Menu(mb) # Video menu, shortcuts to both panels + m.bind_shortcuts_to(v, b) + m.add_item('Open...', self.OnOpen, key=Cmd_ + 'O') + m.add_separator() + self.playItem = m.add_item('Play', self.OnPlay, key=Cmd_ + 'P') # Play/Pause + m.add_item('Stop', self.OnStop, key=Cmd_ + '\b') # BackSpace + m.add_separator() + m.add_item('Zoom In', self.OnZoomIn, key=(Cmd_ + 'Shift++') if _3_9 else '') + m.add_item('Zoom Out', self.OnZoomOut, key=(Cmd_ + '-') if _3_9 else '') + m.add_separator() + m.add_item('Faster', self.OnFaster, key=(Cmd_ + 'Shift+>') if _3_9 else '') + m.add_item('Slower', self.OnSlower, key=(Cmd_ + 'Shift+<') if _3_9 else '') + m.add_separator() + m.add_item('Normal 1X', self.OnNormal, key=Cmd_ + '=') + m.add_separator() + self.muteItem = m.add_item('Mute', self.OnMute, key=Cmd_ + 'M') + m.add_separator() + m.add_item('Snapshot', self.OnSnapshot, key=Cmd_ + 'T') + m.add_separator() + self.fullItem = m.add_item(_FULL_SCREEN, self.OnFull, key=Cmd_ + 'F') + m.add_separator() + m.add_item('Close', self.OnClose, key=Cmd_ + 'W') + mb.add_cascade(menu=m, label='Video') +# self.videoMenu = m + + m = _Tk_Menu(mb) # Video menu, shortcuts to both panels + m.bind_shortcuts_to(v, b) + self.anchorItem = m.add_item(_UN_ANCHORED, self.OnAnchor, key=Cmd_ + 'A') + m.add_separator() + self.opaqueItem = m.add_item(_OPACITY % (self._opacity,), self.OnOpacity, key=Cmd_ + 'Y') + m.add_item('Normal 100%', self.OnOpacity100) + mb.add_cascade(menu=m, label=_BUTTONS) +# self.buttonsMenu = m + + if _isMacOS and self.debug: # Special macOS "windows" menu + # "Providing a Window Menu" + # XXX Which (virtual) events are generated other than Configure? + m = _Tk_Menu(mb, name='window') # must be name='window' + mb.add_cascade(menu=m, label='Windows') # VLC player - args = [] - if _isLinux: - args.append('--no-xlib') + args = ['--no-xlib'] if _isLinux else [] self.Instance = vlc.Instance(args) self.player = self.Instance.media_player_new() - self.parent.bind('', self.OnConfigure) # catch window resize, etc. - self.parent.update() - - # After parent.update() otherwise panel is ignored. - self.buttonsPanel.overrideredirect(True) - self.buttonsPanel_anchored = True # down, under the video panel + b.update_idletasks() + v.update_idletasks() + if self.video: # play video for a second, adjusting the panel + self._play(self.video) + self.after(1000, self.OnPause) +# elif _isMacOS: # +# self._stopped = True +# self._set_opacity() + self.OnTick() # set up the timer - if _dragging: # Detect dragging of the buttons panel. - self.buttonsPanel.bind('', self._Button1Down) - self.buttonsPanel.bind('', self._Button1Motion) - self.buttonsPanel.bind('', self._Button1Up) + # Keep the video panel at least as wide as the buttons panel + # and move it down enough to put the buttons panel above it. + self._BUTTON_H = d = b.winfo_height() + b.minsize(width=_MIN_W, height=d) + v.minsize(width=_MIN_W, height=0) + _, w, h, _, y = _geometry5(v) + y = int(y) + d + _BANNER_H + _geometry(v, w, h, _TAB_X, y) + self._anchorPanels() + self._set_volume() + + def _anchorPanels(self, video=False): + # Put the buttons panel under the video + # or the video panel above the buttons + if self._anchored and not self._isFull: + self._debug(self._anchorPanels) + v = self.videoPanel + if _isMacOS and _fullscreen(v): + # macOS green button full-screen? + _fullscreen(v, False) + self.OnFull() + else: + b = self.buttonsPanel + v.update_idletasks() + b.update_idletasks() + h = v.winfo_height() + d = h + _BANNER_H # vertical delta + if video: # move/adjust video panel + w = b.winfo_width() # same as ... + x = b.winfo_x() # ... buttons + y = b.winfo_y() - d # ... and above + g = v + else: # move/adjust buttons panel + h = b.winfo_height() # unchanged + if h > self._BUTTON_H and _fullscreen(b): + # macOS green button full-screen? + _fullscreen(b, False) + h = self._BUTTON_H + w = v.winfo_width() # unchanged + x = v.winfo_x() # same as the video + y = v.winfo_y() + d # below the video + g = b +# _g = g._g + _geometry(g, max(w, _MIN_W), h, x, y) + if video: # and g._g != _g: + self._set_aspect_ratio(True) + + def _bind_events(self, panel): + # set up handlers for several events + try: + p = panel + p_ = p.protocol + except AttributeError: + p = p.master # == p.parent + p_ = p.protocol + if _isWindows: # OK for macOS + p_('WM_DELETE_WINDOW', self.OnClose) +# Event Types + p.bind('', self.OnConfigure) # window resize, position, etc. + # needed on macOS to catch window close events + p.bind('', self.OnClose) # window half-dead +# p.bind('', self.OnActive) # window activated +# p.bind('', self.OffActive) # window deactivated + p.bind('', self.OnFocus) # getting keyboard focus +# p.bind('', self.OffFocus) # losing keyboard focus +# p.bind('', self.OnReturn) # highlighted button + if _isMacOS: + p.bind('', self.OnClose) +# p.bind('', self.OnReturn) # highlighted button + # attrs holding the most recently set _geometry ... + assert not hasattr(panel, '_g') + panel._g = '' # ... as a sC{str} and ... + assert not hasattr(panel, '_whxy') + panel._whxy = () # ... 4-tuple of C{ints}s + + def _ButtonsPanel(self): + # create panel with buttons and sliders + b = _Tk_Toplevel(self.parent, name='buttons') + t = '%s - %s' % (self._title, _BUTTONS) + b.title(t) # '' removes the window banner + b.resizable(True, False) + + f = _Tk_Frame(b) + # button are too small on Windows if width is given + p = _Tk_Button(f, label='Play', command=self.OnPlay) + # width=len('Pause'), underline=0 + s = _Tk_Button(f, label='Stop', command=self.OnStop) + m = _Tk_Button(f, label='Mute', command=self.OnMute) + # width=len('Unmute'), underline=0 + q = _Tk_Slider(f, command=self.OnPercent, to=100, + label=_VOLUME) # length=170 + p.pack(side=Tk.LEFT, padx=8) + s.pack(side=Tk.LEFT) + m.pack(side=Tk.LEFT, padx=8) + q.pack(fill=Tk.X, padx=4, expand=1) + f.pack(side=Tk.BOTTOM, fill=Tk.X) + + f = _Tk_Frame(b) # new frame? + t = _Tk_Slider(f, command=self.OnTime, to=1000, # millisecs + length=_MIN_W) # label='Time' + t.pack(side=Tk.BOTTOM, fill=Tk.X, expand=1) + f.pack(side=Tk.BOTTOM, fill=Tk.X) - # Keep the video panel at least as wide as thebuttons panel. - self.parent.minsize(width=self._MIN_WIDTH, height=0) + # + # + # b.attributes('-topmost', 1) - self._AnchorPanels(force=True) + # self.videoPanel.update() # needed to ... +# # b.overrideredirect(True) # ignore the panel + b.update_idletasks() - self.OnTick() # set up the timer + self.muteButton = m + self.playButton = p + self.timeSlider = t + self.percentSlider = q + return b - if self.video: # play for a second - self.OnPlay() - self.parent.after(1000, self.OnPause) - - def _Button1Down(self, *unused): # only if -dragging - self._debug(self._Button1Down) - # Left-mouse-button pressed inside the buttons - # panel, but not in and over a slider-/button. - self.buttonsPanel_clicked = True - self.buttonsPanel_dragged = False - - def _Button1Motion(self, *unused): # only if -dragging - self._debug(self._Button1Motion) - # Mouse dragged, moved with left-mouse-button down? - self.buttonsPanel_dragged = self.buttonsPanel_clicked - - def _Button1Up(self, *unused): # only if -dragging - self._debug(self._Button1Up) - # Left-mouse-button release - if self.buttonsPanel_clicked: - if self.buttonsPanel_dragged: - # If the mouse was dragged in the buttons - # panel on the background, un-/anchor it. - self.OnAnchor() -# if _dragged: -# self.buttonsPanel.unbind('') -# self.buttonsPanel.unbind('') -# self.buttonsPanel.unbind('') - self.buttonsPanel_clicked = False - self.buttonsPanel_dragged = False - - def _debug(self, where, **kwds): + def _debug(self, where, *event, **kwds): # Print where an event is are handled. if self.debug: self._debugs += 1 - d = dict(anchored=self.buttonsPanel_anchored, - clicked=self.buttonsPanel_clicked, - dragged=self.buttonsPanel_dragged, - playing=self.player.is_playing(), - stopped=self._stopped) + d = dict(anchored=self._anchored, + isFull=bool(self._isFull), + opacity=self._opacity, + opaque=self._opaque, + stopped=self._stopped, + volume=self._volume) + p = self.player + if p and p.get_media(): + d.update(playing=p.is_playing(), + rate=p.get_rate(), + scale=p.video_get_scale(), + scaleX=self._scaleX) + try: # final OnClose may throw TclError + d.update(Buttons=_geometry1(self.buttonsPanel)) + d.update( Video=_geometry1(self.videoPanel)) + if event: # an event + event = event[0] + d.update(event=event) + w = str(event.widget) +# d.update(widget=type(event.widget)) # may fail + d.update(Widget={'.': 'Video', + '.buttons': _BUTTONS}.get(w, w)) + except (AttributeError, TclError): + pass d.update(kwds) d = ', '.join('%s=%s' % t for t in sorted(d.items())) print('%4s: %s %s' % (self._debugs, where.__name__, d)) - def _AnchorPanels(self, force=False): - # Un-/anchor the buttons under the video panel, at the same width. - self._debug(self._AnchorPanels) - if (force or self.buttonsPanel_anchored): - video = self.parent - h = video.winfo_height() - w = video.winfo_width() - x = video.winfo_x() # i.e. same as the video - y = video.winfo_y() + h + 32 # i.e. below the video - h = self.buttonsPanel.winfo_height() # unchanged - w = max(w, self._MIN_WIDTH) # i.e. same a video width - self.buttonsPanel.geometry('%sx%s+%s+%s' % (w, h, x, y)) + def _frontmost(self): + # Move panels to the front ... temporarily. + for p in (self.videoPanel, self.buttonsPanel): + p.attributes('-topmost', True) + p.update_idletasks() + p.attributes('-topmost', False) + try: # no Toplevel.force_focus + p.force_focus() + except AttributeError: + pass def OnAnchor(self, *unused): - '''Toggle buttons panel anchoring. + '''Toggle anchoring of the panels. ''' - c = self.OnAnchor - self._debug(c) - self.buttonsPanel_anchored = not self.buttonsPanel_anchored - if self.buttonsPanel_anchored: - a = 'Up' - self._AnchorPanels(force=True) - else: # move the panel to the top left corner - a = 'Down' - h = self.buttonsPanel.winfo_height() # unchanged - self.buttonsPanel.geometry('%sx%s+8+32' % (self._MIN_WIDTH, h)) - self.anchorButton.config(text=a, width=len(a)) - a = 'Buttons ' + a - self.fileMenu.entryconfig(self.anchorIndex, label=a, command=c) - # self.fileMenu.bind_shortcut('a', c) # XXX handled - - def OnClose(self, *unused): - '''Closes the window and quit. + self._debug(self.OnAnchor) + self._anchored = not self._anchored + if self._anchored: + self._anchorPanels() + a = _UN_ANCHORED + else: # move the buttons panel to the top-left + b = self.buttonsPanel + h = b.winfo_height() # unchanged + _geometry(b, _MIN_W, h, _TAB_X, _BANNER_H) + a = _ANCHORED + self.anchorItem.config(label=a) + + def OnClose(self, *event): + '''Closes the window(s) and quit. ''' - self._debug(self.OnClose) + self._debug(self.OnClose, *event) # print('_quit: bye') - self.parent.quit() # stops mainloop - self.parent.destroy() # this is necessary on Windows to avoid - # ... Fatal Python Error: PyEval_RestoreThread: NULL tstate - - def OnConfigure(self, *unused): + self.after_cancel(self._tick_a) + self.after_cancel(self._tick_c) + self.after_cancel(self._tick_r) + self.after_cancel(self._tick_s) + self.after_cancel(self._tick_t) + self.after_cancel(self._tick_z) + v = self.videoPanel + v.update_idletasks() + self.quit() # stops .mainloop + + def OnConfigure(self, event): '''Some widget configuration changed. ''' - self._debug(self.OnConfigure) - # - self._geometry = '' # force .OnResize in .OnTick, recursive? - self._AnchorPanels() + w, T = event.widget, event.type # int + if T == _T_CONFIGURE and w.winfo_toplevel() is w: + # i.e. w is videoFrame/Panel or buttonsPanel + if w is self.videoPanel: + a = self._set_aspect_ratio # force=True + elif w is self.buttonsPanel and self._anchored: + a = self._anchorPanels # video=True + else: + a = None + # prevent endless, recursive onConfigure events due to changing + # the buttons- and videoPanel geometry, especially on Windows + if a and w._whxy != (event.width, event.height, event.x, event.y): + self.after_cancel(self._tick_c) + self._debug(self.OnConfigure, event) + self._tick_c = self.after(250, a, True) + + def OnFaster(self, *event): + '''Speed the video up by 25%. + ''' + self._set_rate(1.25, *event) + self._debug(self.OnFaster) + + def OnFocus(self, *unused): + '''Got the keyboard focus. + ''' + self._debug(self.OnFocus) + self._frontmost() +# self._set_aspect_ratio() +# self._wiggle() + + def OnFull(self, *unused): + '''Toggle full/off screen. + ''' + self._debug(self.OnFull) + # + # self.after_cancel(self._tick_t) + v = self.videoPanel + if not _fullscreen(v): + self._isFull = _geometry1(v) + _fullscreen(v, True) # or .wm_attributes + v.bind('', self.OnFull) + f = _FULL_OFF + else: + _fullscreen(v, False) + v.unbind('') + _geometry(v, self._isFull) + self._isFull = '' + self._anchorPanels() + f = _FULL_SCREEN + self.fullItem.config(label=f) def OnMute(self, *unused): '''Mute/Unmute audio. ''' + if self._stopped or self._opaque: + return # button.disabled self._debug(self.OnMute) - self.buttonsPanel_clicked = False # audio un/mute may be unreliable, see vlc.py docs. - self.volMuted = m = not self.volMuted # self.player.audio_get_mute() + self._muted = m = not self._muted # self.player.audio_get_mute() self.player.audio_set_mute(m) u = 'Unmute' if m else 'Mute' - self.fileMenu.entryconfig(self.muteIndex, label=u) - self.muteButton.config(text=u) - # update the volume slider text - self.OnVolume() + # i = u.index('m' if m else 'M') # 2 if m else 0 + self.muteItem.config(label=u) + self.muteButton.config(label=u) # width=len(u), underline=i + self.OnPercent() # re-label the slider - def OnOpen(self, *unused): - '''Pop up a new dialow window to choose a file, then play the selected file. + def OnNormal(self, *unused): + '''Normal speed and 1X zoom. ''' - # if a file is already running, then stop it. - self.OnStop() - # Create a file dialog opened in the current home directory, where - # you can display all kind of files, having as title 'Choose a video'. - video = askopenfilename(initialdir = Path(os.path.expanduser('~')), - title = 'Choose a video', - filetypes = (('all files', '*.*'), - ('mp4 files', '*.mp4'), - ('mov files', '*.mov'))) - self._Play(video) - - def _Pause_Play(self, playing): - # re-label menu item and button, adjust callbacks - p = 'Pause' if playing else 'Play' - c = self.OnPlay if playing is None else self.OnPause # PYCHOK attr - self.fileMenu.entryconfig(self.playIndex, label=p, command=c) - # self.fileMenu.bind_shortcut('p', c) # XXX handled - self.playButton.config(text=p, command=c) - self._stopped = False - - def _Play(self, video): - # helper for OnOpen and OnPlay - if os.path.isfile(video): # Creation - m = self.Instance.media_new(str(video)) # Path, unicode - self.player.set_media(m) - self.parent.title('tkVLCplayer - %s' % (os.path.basename(video),)) + self._frontmost() + self._set_rate(0.0) + self._set_zoom(0.0) +# self._wiggle() + self._set_aspect_ratio(True) + self._debug(self.OnNormal) + + def OnOpacity(self, *unused): + '''Use the percent slider to adjust the opacity. + ''' + self.muteButton.disabled(True) # greyed out? + self.muteItem.disabled(True) # greyed out? + self._opaque = True + self._set_opacity() + self._debug(self.OnOpacity) + + def OnOpacity100(self, *unused): + '''Set the opacity to 100%. + ''' + self._frontmost() + self._set_opacity(100) + self._debug(self.OnOpacity100) - # set the window id where to render VLC's video output - h = self.canvas.winfo_id() # .winfo_visualid()? - if _isWindows: - self.player.set_hwnd(h) - elif _isMacOS: - # XXX 1) using the videoPanel.winfo_id() handle - # causes the video to play in the entire panel on - # macOS, covering the buttons, sliders, etc. - # XXX 2) .winfo_id() to return NSView on macOS? - v = _GetNSView(h) - if v: - self.player.set_nsobject(v) - else: - self.player.set_xwindow(h) # plays audio, no video - else: - self.player.set_xwindow(h) # fails on Windows - # FIXME: this should be made cross-platform - self.OnPlay(None) + def OnOpen(self, *unused): + '''Show the file dialog to choose a video, then play it. + ''' + self._debug(self.OnOpen) + self._reset() + # XXX ... +[CATransaction synchronize] called within transaction + v = askopenfilename(initialdir=os.path.expanduser('~'), + title='Choose a video', + filetypes=(('all files', '*.*'), + ('mp4 files', '*.mp4'), + ('mov files', '*.mov'))) + self._play(os.path.expanduser(v)) + self._set_aspect_ratio(True) def OnPause(self, *unused): '''Toggle between Pause and Play. ''' self._debug(self.OnPause) - self.buttonsPanel_clicked = False - if self.player.get_media(): - self._Pause_Play(not self.player.is_playing()) - self.player.pause() # toggles + p = self.player + if p.get_media(): + self._pause_play(not p.is_playing()) +# self._wiggle() + p.pause() # toggles + + def OnPercent(self, *unused): + '''Percent slider changed, adjust the opacity or volume. + ''' + self._debug(self.OnPercent) + s = max(0, min(100, self.percentSlider.get())) + if self._opaque or self._stopped: + self._set_opacity(s) + else: + self._set_volume(s) def OnPlay(self, *unused): - '''Play video, if not loaded, open the dialog window. + '''Play video, if there's no video to play or + playing, show a Tk.FileDialog to select one ''' self._debug(self.OnPlay) - self.buttonsPanel_clicked = False - # if there's no video to play or playing, - # open a Tk.FileDialog to select a file - if not self.player.get_media(): + p = self.player + m = p.get_media() + if not m: if self.video: - self._Play(os.path.expanduser(self.video)) + self._play(self.video) self.video = '' else: self.OnOpen() - # Try to play, if this fails display an error message - elif self.player.play(): # == -1 - self.showError('Unable to play the video.') + elif p.play(): # == -1, play failed + self._showError('play ' + repr(m)) else: - self._Pause_Play(True) - # set volume slider to audio level - vol = self.player.audio_get_volume() - if vol > 0: - self.volVar.set(vol) - self.volSlider.set(vol) - self.OnResize() - - def OnResize(self, *unused): - '''Adjust the video panel to the video aspect ratio. + self._pause_play(True) + if _isMacOS: + self._wiggle() + + def OnSlower(self, *event): + '''Slow the video down by 20%. ''' - self._debug(self.OnResize) - g = self.parent.geometry() - if g != self._geometry and self.player: - u, v = self.player.video_get_size() # often (0, 0) - if v > 0 and u > 0: - # get window size and position - g, x, y = g.split('+') - w, h = g.split('x') - # alternatively, use .winfo_... - # w = self.parent.winfo_width() - # h = self.parent.winfo_height() - # x = self.parent.winfo_x() - # y = self.parent.winfo_y() - # use the video aspect ratio ... - if u > v: # ... for landscape - # adjust the window height - h = round(float(w) * v / u) - else: # ... for portrait - # adjust the window width - w = round(float(h) * u / v) - self.parent.geometry('%sx%s+%s+%s' % (w, h, x, y)) - self._geometry = self.parent.geometry() # actual - self._AnchorPanels() - - def OnScreen(self, *unused): - '''Toggle full/off screen. + self._set_rate(0.80, *event) + self._debug(self.OnSlower) + + def OnSnapshot(self, *unused): + '''Take a snapshot and save it (as .PNG only). ''' - c = self.OnScreen - self._debug(c) - # - f = not self.parent.attributes('-fullscreen') # or .wm_attributes - if f: - self._previouscreen = self.parent.geometry() - self.parent.attributes('-fullscreen', f) # or .wm_attributes - self.parent.bind('', c) - f = 'Off' - else: - self.parent.attributes('-fullscreen', f) # or .wm_attributes - self.parent.geometry(self._previouscreen) - self.parent.unbind('') - f = 'Full' - f += ' Screen' - self.fileMenu.entryconfig(self.fullIndex, label=f, command=c) - # self.fileMenu.bind_shortcut('f', c) # XXX handled + p = self.player + if p and p.get_media(): + self._snapshots += 1 + S = 'Snapshot%s' % (self._snapshots,) + s = '%s-%s.PNG' % (self._title, S) # PNG only + if p.video_take_snapshot(0, s, 0, 0): + self._showError('take ' + S) def OnStop(self, *unused): - '''Stop the player, resets media. + '''Stop the player, clear panel, etc. ''' self._debug(self.OnStop) - self.buttonsPanel_clicked = False - if self.player: - self.player.stop() - self._Pause_Play(None) - # reset the time slider - self.timeSlider.set(0) - self._stopped = True - # XXX on macOS libVLC prints these error messages: - # [h264 @ 0x7f84fb061200] get_buffer() failed - # [h264 @ 0x7f84fb061200] thread_get_buffer() failed - # [h264 @ 0x7f84fb061200] decode_slice_header error - # [h264 @ 0x7f84fb061200] no frame! + self._reset() def OnTick(self): - '''Timer tick, update the time slider to the video time. + '''Udate the time slider with the video time. ''' - if self.player: - # since the self.player.get_length may change while - # playing, re-set the timeSlider to the correct range - t = self.player.get_length() * 1e-3 # to seconds - if t > 0: - self.timeSlider.config(to=t) - - t = self.player.get_time() * 1e-3 # to seconds - # don't change slider while user is messing with it - if t > 0 and time.time() > (self.timeSliderUpdate + 2): - self.timeSlider.set(t) - self.timeSliderLast = int(self.timeVar.get()) - # start the 1 second timer again - self.parent.after(1000, self.OnTick) - # adjust window to video aspect ratio, done periodically - # on purpose since the player.video_get_size() only - # returns non-zero sizes after playing for a while - self.OnResize() + p = self.player + if p: + s = self.timeSlider + if self._length > 0: + if not self._sliding: # see .OnTime + t = max(0, p.get_time() // _TICK_MS) + if t != s.get(): + s.set(t) + self._set_buttons_title(t) + else: # get video length in millisecs + t = p.get_length() + if t > 0: + self._length = t = max(1, t // _TICK_MS) + self._lengthstr = _hms(t, secs=' secs') + s.config(to=t) # tickinterval=t / 5) + # re-start the 1/4-second timer + self._tick_t = self.after(250, self.OnTick) def OnTime(self, *unused): - if self.player: - t = self.timeVar.get() - if self.timeSliderLast != int(t): - # This is a hack. The timer updates the time slider and - # that change causes this rtn (the 'slider has changed') - # to be invoked. I can't tell the difference between the - # user moving the slider manually and the timer changing - # the slider. When the user moves the slider, tkinter - # only notifies this method about once per second and - # when the slider has quit moving. - # Also, the tkinter notification value has no fractional - # seconds. The timer update rtn saves off the last update - # value (rounded to integer seconds) in timeSliderLast if - # the notification time (sval) is the same as the last saved - # time timeSliderLast then we know that this notification is - # due to the timer changing the slider. Otherwise the - # notification is due to the user changing the slider. If - # the user is changing the slider then I have the timer - # routine wait for at least 2 seconds before it starts - # updating the slider again (so the timer doesn't start - # fighting with the user). - self.player.set_time(int(t * 1e3)) # milliseconds - self.timeSliderUpdate = time.time() - - def OnVolume(self, *unused): - '''Volume slider changed, adjust the audio volume. + '''Time slider has been moved by user. ''' - self._debug(self.OnVolume) - self.buttonsPanel_clicked = False - self.buttonsPanel_dragged = False - vol = min(self.volVar.get(), 100) - v_M = '%d%s' % (vol, ' (Muted)' if self.volMuted else '') - self.volSlider.config(label='Volume ' + v_M) - if self.player and not self._stopped: - # .audio_set_volume returns 0 if success, -1 otherwise, - # e.g. if the player is stopped or doesn't have media - if self.player.audio_set_volume(vol): # and self.player.get_media(): - self.showError('Failed to set the volume: %s.' % (v_M,)) + if self.player and self._length: + self._sliding = True # slider moving, see .OnTick + self.after_cancel(self._tick_s) + t = self.timeSlider.get() + self._tick_s = self.after_idle(self._set_time, t * _TICK_MS) + self._set_buttons_title(t) + self._debug(self.OnTime, tensecs=t) + + def OnZoomIn(self, *event): + '''Zoom in by 25%. + ''' + self._set_zoom(1.25, *event) + self._debug(self.OnZoomIn) + + def OnZoomOut(self, *event): + '''Zoom out by 20%. + ''' + self._set_zoom(0.80, *event) + self._debug(self.OnZoomOut) + + def _pause_play(self, playing): + # re-label menu item and button, adjust callbacks + p = 'Pause' if playing else 'Play' + c = self.OnPlay if playing is None else self.OnPause # PYCHOK attr + self.playButton.config(label=p, command=c) + self.playItem.config(label=p, command=c) + self.muteButton.disabled(False) + self.muteItem.disabled(False) + self._stopped = self._opaque = False + self._set_buttons_title() + self._set_opacity() # no re-label + self._set_volume() + self._set_aspect_ratio(True) + + def _play(self, video): + # helper for OnOpen and OnPlay + if os.path.isfile(video): # Creation + m = self.Instance.media_new(str(video)) # unicode + p = self.player + p.set_media(m) + t = '%s - %s' % (self._title, os.path.basename(video)) + self.videoPanel.title(t) +# self.buttonsPanel.title(t) + + # get the window handle for VLC to render the video + h = self.videoCanvas.winfo_id() # .winfo_visualid()? + if _isWindows: + p.set_hwnd(h) + elif _isMacOS: + # (1) the handle on macOS *must* be an NSView + # (2) the video fills the entire panel, covering + # all frames, buttons, sliders, etc. inside it + ns = _GetNSView(h) + if ns: + p.set_nsobject(ns) + else: # no libtk: no video, only audio + p.set_xwindow(h) + else: # *nix, Xwindows + p.set_xwindow(h) # fails on Windows + self.OnPlay(None) - def showError(self, message): + def _reset(self): + # stop playing, clear panel + p = self.player + if p: + p.stop() + self.timeSlider.set(0) + self._pause_play(None) + self._sliding = False + self._stopped = True + self.OnOpacity() + + def _set_aspect_ratio(self, force=False): + # set the video panel aspect ratio and re-anchor + p = self.player + if p and not self._isFull: + v = self.videoPanel + g, w, h, x, y = _geometry5(v) + if force or g != v._g: # update + self.after_cancel(self._tick_a) + a, b = p.video_get_size(0) # often (0, 0) + if b > 0 and a > 0: + # adjust the video panel ... + if a > b: # ... landscape height + h = round(float(w) * b / a) + else: # ... or portrait width + w = round(float(h) * a / a) + _g = _geometry(v, w, h, x, y) + self._debug(self._set_aspect_ratio, a=a, b=b) + if self._anchored and (force or _g != g): + self._anchorPanels() + # redo periodically since (1) player.video_get_size() + # only returns non-zero width and height after playing + # for a while and (2) avoid too frequent updates during + # manual resizing of the video panel + self._tick_a = self.after(500, self._set_aspect_ratio) + + def _set_buttons_title(self, *tensecs): + # set the buttons panel title + T, s = self._length, self._lengthstr + if s and T: + t = tensecs[0] if tensecs else self.timeSlider.get() + t = _hms(t) if t < T else s + t = '%s - %s / %s%s%s' % (self._title, t, s, self._scaleXstr, self._ratestr) + else: # reset panel title + t = '%s - %s' % (self._title, _BUTTONS) + self._length = 0 +# self._lengthstr = '' + self.buttonsPanel.title(t) + + def _set_opacity(self, *percent): # 100% fully opaque + # set and re-label the opacity, panels and menu item + if percent: + self._opacity = p = percent[0] + else: + p = self._opacity + a = max(0.2, min(1, p * 1e-2)) + self.videoPanel.attributes('-alpha', a if self._stopped else 1) + self.buttonsPanel.attributes('-alpha', a) +# if _isMacOS: # +# self.buttonsPanel.attributes('-transparent', a) + s = _OPACITY % (p,) + self.opaqueItem.config(label=s) + if self._opaque or self._stopped: + self._set_percent(p, label=s) + + def _set_percent(self, percent, **cfg): + # set and re-label the slider + self.percentSlider.config(**cfg) + self.percentSlider.set(percent) + + def _set_rate(self, factor, *event): + # change the video rate + p = self.player + self.after_cancel(self._tick_r) + if not event: # delay the menu event as a false key event ... + # ... and possibly overwritten by the actual key event + self._tick_r = self.after(3, self._set_rate, factor, False) + elif p: + r = p.get_rate() * factor + if r > 0: + r = max(0.2, min(10.0, r)) + t = ' - %d%%' % (int(r * 100),) + else: + r, t = 1.0, '' + p.set_rate(r) + self._rate = r + self._ratestr = t + self._set_buttons_title() + + def _set_time(self, millisecs): + # set player to time + p = self.player + if p: + p.set_time(millisecs) + self._sliding = False # see .OnTick + + def _set_volume(self, *volume): + # set and re-label the volume + if volume: + self._volume = v = volume[0] + else: + v = self._volume + m = ' (Muted)' if self._muted else '' + V = '%s %s%%' % (_VOLUME, v) + self._set_percent(v, label=V + m) + p = self.player + if p and p.is_playing() and not self._stopped: + # .audio_set_volume returns 0 on success, -1 otherwise, + # e.g. if the player is stopped or doesn't have media + if p.audio_set_volume(v): # and p.get_media(): + self._showError('set ' + V) + + def _set_zoom(self, factor, *event): + # zoom the video in/out, see cocoavlc.py + p = self.player + self.after_cancel(self._tick_z) + if not event: # delay the menu event as a false key event ... + # ... and possibly overwritten by the actual key event + self._tick_z = self.after(3, self._set_zoom, factor, False) + elif p: + x = self._scaleX * factor + if x > 1: + s = x + t = ' - %.1fX' % (x,) + else: + x, s, t = 1, 0.0, '' + p.video_set_scale(s) +# self.videoPanel.update_idletasks() + self._scaleX = x + self._scaleXstr = t + self._set_buttons_title() + + def _showError(self, verb): '''Display a simple error dialog. ''' - self.OnStop() - showerror(self.parent.title(), message) + t = 'Unable to %s' % (verb,) + showerror(self._title, t) + # sys.exit(t) + + def _VideoPanel(self): + # create panel to play video + v = _Tk_Frame(self.parent) + c = _Tk_Canvas(v) # takefocus=True + c.pack(fill=Tk.BOTH, expand=1) + v.pack(fill=Tk.BOTH, expand=1) + v.update_idletasks() + + self.videoCanvas = c + self.videoFrame = v + # root is used for updates, NOT ... + return self.parent # ... the frame + + def _wiggle(self, d=4): + # wiggle the video to fill the window on macOS + if not self._isFull: + v = self.videoPanel + g, w, h, x, y = _geometry5(v) + w = int(w) + d + # x = int(x) - d + # h = int(h) + d + if _geometry(v, w, h, x, y) != g: + self.after_idle(_geometry, v, g) + if d > 1: # repeat a few times + self.after(100, self._wiggle, d - 1) + + +def print_version(name=''): # imported by psgvlc.py + # show all versions, this module, tkinter, libtk, vlc.py, libvlc, etc. + + # sample output on macOS: + + # % python3 ./tkvlc.py -v + # tkvlc.py: 22.12.28 + # tkinter: 8.6 + # libTk: /Library/Frameworks/Python.framework/Versions/3.11/lib/libtk8.6.dylib + # is_TK: aqua, isAquaTk, isCocoaTk + # vlc.py: 3.0.12119 (Mon May 31 18:25:17 2021 3.0.12) + # libVLC: 3.0.16 Vetinari (0x3001000) + # plugins: /Applications/VLC.app/Contents/MacOS/plugins + # Python: 3.11.0 (64bit) macOS 13.0.1 arm64 + + # sample output on Windows: + + # PS C: python3 .\tkvlc.py -v + # tkvlc.py: 22.12.28 + # tkinter: 8.6 + # libTk: N/A + # is_TK: win32 + # vlc.py: 3.0.12119 (Mon May 31 18:25:17 2021 3.0.12) + # libVLC: 3.0.18 Vetinari (0x3001200) + # plugins: C:\Program Files\VideoLAN\VLC + # Python: 3.11.0 (64bit) Windows 10 + + # see or private property + r = Tk.Tk() + t = r.tk.call('tk', 'windowingsystem'), # r._windowingsystem + r.destroy() + if _isMacOS: + try: + from idlelib import macosx + m = macosx.__dict__ + t += tuple(sorted(n for n, t in m.items() if n.startswith('is') and + n.endswith('Tk') and + callable(t) and t())) + except ImportError: # Python 10: no test.support ... + pass + t = ', '.join(t) or 'N/A' + + n = os.path.basename(name or __file__) + for t in ((n, __version__), (Tk.__name__, _Tk_Version), ('libTk', libtk), ('is_Tk', t)): + print('%s: %s' % t) + + try: + vlc.print_version() + vlc.print_python() + except AttributeError: + try: + os.system(sys.executable + ' -m vlc -v') + except OSError: + pass if __name__ == '__main__': # MCCABE 13 @@ -647,43 +1290,29 @@ def showError(self, message): while len(sys.argv) > 1: arg = sys.argv.pop(1) - if arg.lower() in ('-v', '--version'): - # show all versions, this vlc.py, libvlc, etc. (sample output on macOS): - # % python3 ./tkvlc.py -v - # tkvlc.py: 22.11.10 - # tkinter: 8.6 - # libTk: /Library/Frameworks/Python.framework/Versions/3.11/lib/libtk8.6.dylib - # vlc.py: 3.0.12119 (Mon May 31 18:25:17 2021 3.0.12) - # libVLC: 3.0.16 Vetinari (0x3001000) - # plugins: /Applications/VLC.app/Contents/MacOS/plugins - # Python: 3.11.0 (64bit) macOS 13.0.1 arm64 - for t in ((_argv0, __version__), (Tk.__name__, Tk.TkVersion), ('libTk', libtk)): - print('%s: %s' % t) - try: - vlc.print_version() - vlc.print_python() - except AttributeError: - pass + if arg in ('-v', '--version'): + print_version() sys.exit(0) - elif '-debug'.startswith(arg) and len(arg) > 2: + elif '-debug'.startswith(arg) and len(arg) > 3: _debug = True - elif '-dragging'.startswith(arg) and len(arg) > 2: - _dragging = True # detect dragging in buttons panel for Buttons Up/Down elif arg.startswith('-'): - print('usage: %s [-v | --version] [-debug] [-dragging] []' % (_argv0,)) + print('usage: %s [-v | --version] [-debug] []' % (_argv0,)) sys.exit(1) elif arg: # video file _video = os.path.expanduser(arg) if not os.path.isfile(_video): - print('%s error: no such file: %r' % (_argv0, arg)) + print('%s error, no such file: %r' % (_argv0, arg)) sys.exit(1) - # Create a Tk.App() to handle the windowing event loop - root = Tk.Tk() + root = Tk.Tk() # create a Tk.App() player = Player(root, video=_video, debug=_debug) - root.protocol('WM_DELETE_WINDOW', player.OnClose) # XXX unnecessary (on macOS) - if _isWindows: # see + if _isWindows: # see function _test() at the bottom of ... + # root.iconify() root.update() root.deiconify() - root.mainloop() + root.mainloop() # forever + root.destroy() # this is necessary on Windows to avoid ... + # ... Fatal Python Error: PyEval_RestoreThread: NULL tstate + else: + root.mainloop() # forever diff --git a/generated/3.0/setup.py b/generated/3.0/setup.py index b9d2257..dc10233 100644 --- a/generated/3.0/setup.py +++ b/generated/3.0/setup.py @@ -4,7 +4,7 @@ from setuptools import setup setup(name='python-vlc', - version = '3.0.18122', + version = '3.0.20123', author='Olivier Aubert', author_email='contact@olivieraubert.net', maintainer='Olivier Aubert', @@ -35,5 +35,5 @@ player. Note that it relies on an already present install of VLC. It has been automatically generated from the include files of - vlc 3.0.18, using generator 1.22. + vlc 3.0.20, using generator 1.23. """) diff --git a/generated/3.0/vlc.py b/generated/3.0/vlc.py index 4ba3f51..b9e3cc9 100644 --- a/generated/3.0/vlc.py +++ b/generated/3.0/vlc.py @@ -51,10 +51,10 @@ import logging logger = logging.getLogger(__name__) -__version__ = "3.0.18122" -__libvlc_version__ = "3.0.18" -__generator_version__ = "1.22" -build_date = "Wed Apr 19 17:27:23 2023 3.0.18" +__version__ = "3.0.20123" +__libvlc_version__ = "3.0.20" +__generator_version__ = "1.23" +build_date = "Sat Nov 4 17:10:43 2023 3.0.20" # The libvlc doc states that filenames are expected to be in UTF8, do # not rely on sys.getfilesystemencoding() which will be confused,