diff options
Diffstat (limited to 'mvc')
126 files changed, 0 insertions, 20656 deletions
diff --git a/mvc/__init__.py b/mvc/__init__.py deleted file mode 100644 index 94760ce..0000000 --- a/mvc/__init__.py +++ /dev/null @@ -1,37 +0,0 @@ -import os - -import multiprocessing -from mvc import converter -from mvc import conversion -from mvc import signals -from mvc import video - -VERSION = '3.0a' - -class Application(signals.SignalEmitter): - - def __init__(self, simultaneous=None): - signals.SignalEmitter.__init__(self) - if simultaneous is None: - try: - simultaneous = multiprocessing.cpu_count() - except NotImplementedError: - pass - self.converter_manager = converter.ConverterManager() - self.conversion_manager = conversion.ConversionManager(simultaneous) - self.started = False - - def startup(self): - if self.started: - return - self.converter_manager.startup() - self.started = True - - def start_conversion(self, filename, converter_id): - self.startup() - converter = self.converter_manager.get_by_id(converter_id) - v = video.VideoFile(filename) - return self.conversion_manager.start_conversion(v, converter) - - def run(self): - raise NotImplementedError diff --git a/mvc/__main__.py b/mvc/__main__.py deleted file mode 100644 index 1992c4e..0000000 --- a/mvc/__main__.py +++ /dev/null @@ -1,9 +0,0 @@ -if __name__ == "__main__": - try: - from mvc.ui.widgets import Application - except ImportError: - from mvc.ui.console import Application - from mvc.widgets import app - from mvc.widgets import initialize - app.widgetapp = Application() - initialize(app.widgetapp) diff --git a/mvc/basicconverters.py b/mvc/basicconverters.py deleted file mode 100644 index 4cb4c0d..0000000 --- a/mvc/basicconverters.py +++ /dev/null @@ -1,141 +0,0 @@ -import logging -import re - -from mvc import converter - -class WebM_UHD(converter.FFmpegConverterInfo1080p): - media_type = 'format' - extension = 'webm' - parameters = ('-f webm -vcodec libvpx -g 120 -lag-in-frames 16 ' - '-deadline good -cpu-used 0 -vprofile 0 -qmax 51 -qmin 11 ' - '-slices 4 -b:v 4M -acodec libvorbis -ab 128k ' - '-ar 44100') - -class WebM_HD(converter.FFmpegConverterInfo720p): - media_type = 'format' - extension = 'webm' - parameters = ('-f webm -vcodec libvpx -g 120 -lag-in-frames 16 ' - '-deadline good -cpu-used 0 -vprofile 0 -qmax 51 -qmin 11 ' - '-slices 4 -b:v 2M -acodec libvorbis -ab 112k ' - '-ar 44100') - -class WebM_SD(converter.FFmpegConverterInfo480p): - media_type = 'format' - extension = 'webm' - parameters = ('-f webm -vcodec libvpx -g 120 -lag-in-frames 16 ' - '-deadline good -cpu-used 0 -vprofile 0 -qmax 53 -qmin 0 ' - '-b:v 768k -acodec libvorbis -ab 112k ' - '-ar 44100') - -class MP4(converter.FFmpegConverterInfo): - media_type = 'format' - extension = 'mp4' - parameters = ('-acodec aac -ab 96k -vcodec libx264 -preset slow ' - '-f mp4 -crf 22') - -class MP3(converter.FFmpegConverterInfo): - media_type = 'format' - extension = 'mp3' - parameters = '-f mp3 -ac 2' - audio_only = True - -class OggVorbis(converter.FFmpegConverterInfo): - media_type = 'format' - extension = 'ogg' - parameters = '-f ogg -vn -acodec libvorbis -aq 60' - audio_only = True - -class OggTheora(converter.FFmpegConverterInfo): - media_type = 'format' - extension = 'ogv' - parameters = '-f ogg -codec:v libtheora -qscale:v 7 -codec:a libvorbis -qscale:a 5' - -class DNxHD_1080(converter.FFmpegConverterInfo1080p): - media_type = 'format' - extension = 'mov' - parameters = ('-r 23.976 -f mov -vcodec dnxhd -b:v ' - '175M -acodec pcm_s16be -ar 48000') - -class DNxHD_720(converter.FFmpegConverterInfo720p): - media_type = 'format' - extension = 'mov' - parameters = ('-r 23.976 -f mov -vcodec dnxhd -b:v ' - '175M -acodec pcm_s16be -ar 48000') - -class PRORES_720(converter.FFmpegConverterInfo720p): - media_type = 'format' - extension = 'mov' - parameters = ('-f mov -vcodec prores -profile 2 ' - '-acodec pcm_s16be -ar 48000') - -class PRORES_1080(converter.FFmpegConverterInfo1080p): - media_type = 'format' - extension = 'mov' - parameters = ('-f mov -vcodec prores -profile 2 ' - '-acodec pcm_s16be -ar 48000') - -class AVC_INTRA_1080(converter.FFmpegConverterInfo1080p): - media_type = 'format' - extension = 'mov' - parameters = ('-f mov -vcodec libx264 -pix_fmt yuv422p ' - '-crf 0 -intra -b:v 100M -acodec pcm_s16be -ar 48000') - -class AVC_INTRA_720(converter.FFmpegConverterInfo720p): - media_type = 'format' - extension = 'mov' - parameters = ('-f mov -vcodec libx264 -pix_fmt yuv422p ' - '-crf 0 -intra -b:v 100M -acodec pcm_s16be -ar 48000') - -class NullConverter(converter.FFmpegConverterInfo): - media_type = 'format' - extension = None - - def get_parameters(self, video): - params = [] - if not video.audio_only and self.should_copy_video_size(video): - # -vcodec copy copies the video data exactly. Only use it if the - # output video is the same size as the input video (#19664) - params.extend(['-vcodec', 'copy']) - params.extend(['-acodec', 'copy']) - return params - - def should_copy_video_size(self, video): - if self.width is None or self.height is None: - return True - return (video.width == self.width and video.height == self.height) - - def get_extra_arguments(self, video, output): - if not video.container: - logging.warn("sameformat: video.container is None. Using mp4") - container = 'mp4' - elif isinstance(video.container, list): - # XXX: special case mov,mp4,m4a,3gp,3g2,mj2 - container = 'mp4' - else: - container = video.container - return ['-f', container] - -mp3 = MP3('MP3') -ogg_vorbis = OggVorbis('Ogg Vorbis') -audio_formats = ('Audio', [mp3, ogg_vorbis]) - -webm_uhd = WebM_UHD('WebM UHD') -webm_hd = WebM_HD('WebM HD') -webm_sd = WebM_SD('WebM SD') -mp4 = MP4('MP4') -theora = OggTheora('Ogg Theora') - -video_formats = ('Video', [webm_uhd, webm_hd, webm_sd, mp4, theora]) - -dnxhd_1080 = DNxHD_1080('DNxHD 1080p') -dnxhd_720 = DNxHD_720('DNxHD 720p') -prores_1080 = PRORES_1080('Prores Ingest 1080p') -prores_720 = PRORES_720('Prores Ingest 720p') -avc_intra_1080 = PRORES_1080('AVC Intra 1080p') -avc_intra_720 = PRORES_720('AVC Intra 720p') - -ingest_formats = ('Ingest Formats', [dnxhd_1080, dnxhd_720, prores_1080, - prores_720, avc_intra_1080, avc_intra_720]) -null_converter = NullConverter('Same Format') - -converters = [video_formats, audio_formats, ingest_formats, null_converter] diff --git a/mvc/conversion.py b/mvc/conversion.py deleted file mode 100644 index c7aa883..0000000 --- a/mvc/conversion.py +++ /dev/null @@ -1,313 +0,0 @@ -import collections -import errno -import os -import time -import tempfile -import threading -import shutil -import logging - -from mvc import execute -from mvc.utils import line_reader -from mvc.video import get_thumbnail_synchronous -from mvc.widgets import get_conversion_directory - -logger = logging.getLogger(__name__) - -class Conversion(object): - def __init__(self, video, converter, manager, output_dir=None): - self.video = video - self.manager = manager - if output_dir is None: - output_dir = get_conversion_directory() - self.output_dir = output_dir - self.lines = [] - self.thread = None - self.popen = None - self.status = 'initialized' - self.temp_output = None - self.error = None - self.started_at = None - self.duration = None - self.progress = None - self.progress_percent = None - self.create_thumbnail = False - self.eta = None - self.listeners = set() - self.set_converter(converter) - logger.info('created %r', self) - - def set_converter(self, converter): - if self.status != 'initialized': - raise RuntimeError("can't change converter after starting") - self.converter = converter - self.output = os.path.join(self.output_dir, - converter.get_output_filename(self.video)) - - def __repr__(self): - return unicode(self) - - def __str__(self): - return unicode(self).encode('utf8') - - def __unicode__(self): - return u'<Conversion (%s) %r -> %r>' % ( - self.converter.name, self.video.filename, self.output) - - def listen(self, f): - self.listeners.add(f) - - def unlisten(self, f): - self.listeners.remove(f) - - def notify_listeners(self): - self.manager.notify_queue.add(self) - - def run(self): - logger.info('starting %r', self) - try: - self.temp_output = tempfile.mktemp( - dir=os.path.dirname(self.output)) - except EnvironmentError,e : - logger.exception('while creating temp file for %r', - self.output) - self.error = str(e) - self.finalize() - return - logger.info('commandline: %r', ' '.join( - self.get_subprocess_arguments(self.temp_output))) - self.thread = threading.Thread(target=self._thread, - name="Thread:%s" % (self,)) - self.thread.setDaemon(True) - self.thread.start() - - def stop(self): - logger.info('stopping %r', self) - self.error = 'manually stopped' - if self.popen is None: - status = 'canceled' - try: - self.manager.remove(self) - except ValueError: - status = 'failed' - logger.exception('not running and not waiting %s' % (self,)) - self.status = status - return - else: - try: - self.popen.kill() - self.popen.wait() - # set the status transition last, if we had hit an exception - # then we will transition the next state to 'failed' in - # finalize() - self.status = 'canceled' - except EnvironmentError, e: - logger.exception('while stopping %s' % (self,)) - self.error = str(e) - self.popen = None - self.manager.conversion_finished(self) - - def _thread(self): - try: - commandline = self.get_subprocess_arguments(self.temp_output) - self.popen = execute.Popen(commandline, bufsize=1) - self.process_output() - if self.popen: - # if we stop the thread, we can get here after `.stop()` - # finishes. - self.popen.wait() - except OSError, e: - if e.errno == errno.ENOENT: - self.error = '%r does not exist' % ( - self.converter.get_executable(),) - else: - logger.exception('OSError in %s' % (self.thread.name,)) - self.error = str(e) - except Exception, e: - logger.exception('in %s' % (self.thread.name,)) - self.error = str(e) - - if self.create_thumbnail: - self.write_thumbnail_file() - self.finalize() - - def write_thumbnail_file(self): - try: - self._write_thumbnail_file() - except StandardError: - logging.warn("Error writing thumbnail", exc_info=True) - - def _write_thumbnail_file(self): - if self.video.audio_only: - logging.warning("write_thumbnail_file: audio_only=True " - "not writing thumbnail %s", self.video.filename) - return - output_basename = os.path.splitext(os.path.basename(self.output))[0] - logging.info("td: %s ob: %s", self._get_thumbnail_dir(), - output_basename) - thumbnail_path = os.path.join(self._get_thumbnail_dir(), - output_basename + '.png') - logging.info("creating thumbnail: %s", thumbnail_path) - width, height = self.converter.get_target_size(self.video) - get_thumbnail_synchronous(self.video.filename, width, height, - thumbnail_path) - if os.path.exists(thumbnail_path): - logging.info("thumbnail successful: %s", thumbnail_path) - else: - logging.warning("get_thumbnail_synchronous() succeeded, but the " - "thumbnail file is missing!") - - def _get_thumbnail_dir(self): - """Get the directory to store thumbnails in it. - - This method will create the directory if it doesn't exist - """ - thumbnail_dir = os.path.join(self.output_dir, 'thumbnails') - if not os.path.exists(thumbnail_dir): - os.mkdir(thumbnail_dir) - return thumbnail_dir - - def calc_progress_percent(self): - if not self.duration: - return 0.0 - - if self.create_thumbnail: - # assume that thumbnail creation takes as long as 2 seconds of - # video processing - effective_duration = self.duration + 2.0 - else: - effective_duration = self.duration - return self.progress / effective_duration - - def process_output(self): - self.started_at = time.time() - self.status = 'converting' - # We use line_reader, rather than just iterating over the file object, - # because iterating over the file object gives us all the lines when - # the process ends, and we're looking for real-time updates. - for line in line_reader(self.popen.stdout): - self.lines.append(line) # for debugging, if needed - try: - status = self.converter.process_status_line(self.video, line) - except StandardError: - logging.warn("error in process_status_line()", exc_info=True) - continue - if status is None: - continue - updated = set() - if 'finished' in status: - self.error = status.get('error', None) - break - if 'duration' in status: - updated.update(('duration', 'progress')) - self.duration = float(status['duration']) - if self.progress is None: - self.progress = 0.0 - if 'progress' in status: - updated.add('progress') - self.progress = min(float(status['progress']), - self.duration) - if 'eta' in status: - updated.add('eta') - self.eta = float(status['eta']) - - if updated: - self.progress_percent = self.calc_progress_percent() - if 'eta' not in updated: - if self.duration and 0 < self.progress_percent < 1.0: - progress = self.progress_percent * 100 - elapsed = time.time() - self.started_at - time_per_percent = elapsed / progress - self.eta = float( - time_per_percent * (100 - progress)) - else: - self.eta = 0.0 - - self.notify_listeners() - - def finalize(self): - self.progress = self.duration - self.progress_percent = 1.0 - self.eta = 0 - if self.error is None: - self.status = 'staging' - self.notify_listeners() - try: - self.converter.finalize(self.temp_output, self.output) - except EnvironmentError, e: - logger.exception('while trying to move %r to %r after %s', - self.temp_output, self.output, self) - self.error = str(e) - self.status = 'failed' - else: - self.status = 'finished' - else: - if self.temp_output is not None: - try: - os.unlink(self.temp_output) - except EnvironmentError: - pass # ignore errors removing temp files; they may not have - # been created - if self.status != 'canceled': - self.status = 'failed' - if self.status != 'canceled': - self.notify_listeners() - logger.info('finished %r; status: %s', self, self.status) - - def get_subprocess_arguments(self, output): - return ([self.converter.get_executable()] + - list(self.converter.get_arguments(self.video, output))) - -class ConversionManager(object): - def __init__(self, simultaneous=None): - self.notify_queue = set() - self.in_progress = set() - self.waiting = collections.deque() - self.simultaneous = simultaneous - self.running = False - self.create_thumbnails = False - - def get_conversion(self, video, converter, **kwargs): - return Conversion(video, converter, self, **kwargs) - - def remove(self, conversion): - self.waiting.remove(conversion) - - def start_conversion(self, video, converter): - return self.run_conversion(self.get_conversion(video, converter)) - - def run_conversion(self, conversion): - if (self.simultaneous is not None and - len(self.in_progress) >= self.simultaneous): - self.waiting.append(conversion) - else: - self._start_conversion(conversion) - self.running = True - return conversion - - def _start_conversion(self, conversion): - self.in_progress.add(conversion) - conversion.create_thumbnail = self.create_thumbnails - conversion.run() - - def check_notifications(self): - if not self.running: - # don't bother checking if we're not running - return - - self.notify_queue, changed = set(), self.notify_queue - - for conversion in changed: - if conversion.status in ('canceled', 'finished', 'failed'): - self.conversion_finished(conversion) - for listener in conversion.listeners: - listener(conversion) - - def conversion_finished(self, conversion): - self.in_progress.discard(conversion) - while (self.waiting and self.simultaneous is not None and - len(self.in_progress) < self.simultaneous): - c = self.waiting.popleft() - self._start_conversion(c) - if not self.in_progress: - self.running = False diff --git a/mvc/converter.py b/mvc/converter.py deleted file mode 100644 index f845826..0000000 --- a/mvc/converter.py +++ /dev/null @@ -1,278 +0,0 @@ -import json -import logging -import os -import re -import shutil - -from mvc import resources, settings, utils -from mvc.utils import hms_to_seconds - -from mvc.qtfaststart import processor -from mvc.qtfaststart.exceptions import FastStartException - -logger = logging.getLogger(__name__) - -NON_WORD_CHARS = re.compile(r"[^a-zA-Z0-9]+") - -class ConverterInfo(object): - """Describes a particular output converter - - ConverterInfo is the base class for all converters. Subclasses must - implement get_executable() and get_arguments() - - :attribue name: user-friendly name for this converter - :attribute identifier: unique id for this converter - :attribute width: output width for this converter, or None to copy the - input width. This attribute is set to a default on construction, but can - be changed to reflect the user overriding the default. - :attribute height: output height for this converter. Works just like - width - :attribute dont_upsize: should we allow upsizing for conversions? - """ - media_type = None - bitrate = None - extension = None - audio_only = False - - def __init__(self, name, width=None, height=None, dont_upsize=True): - self.name = name - self.identifier = NON_WORD_CHARS.sub("", name).lower() - self.width = width - self.height = height - self.dont_upsize = dont_upsize - - def get_executable(self): - raise NotImplementedError - - def get_arguments(self, video, output): - raise NotImplementedError - - def get_output_filename(self, video): - basename = os.path.basename(video.filename) - name, ext = os.path.splitext(basename) - if ext and ext[0] == '.': - ext = ext[1:] - extension = self.extension if self.extension else ext - return '%s.%s.%s' % (name, self.identifier, extension) - - def get_output_size_guess(self, video): - if not self.bitrate or not video.duration: - return None - if video.duration: - return self.bitrate * video.duration / 8 - - def finalize(self, temp_output, output): - err = None - needs_remove = False - if self.media_type == 'format' and self.extension == 'mp4': - needs_remove = True - logging.debug('generic mp4 format detected. ' - 'Running qtfaststart...') - try: - processor.process(temp_output, output) - except FastStartException: - logging.exception('qtfaststart: exception occurred') - err = EnvironmentError('qtfaststart exception') - else: - try: - shutil.move(temp_output, output) - except EnvironmentError, e: - needs_remove = True - err = e - # If it didn't work for some reason try to clean up the stale stuff. - # And if that doesn't work ... just log, and re-raise the original - # error. - if needs_remove: - try: - os.remove(temp_output) - except EnvironmentError, e: - logging.error('finalize(): cannot remove stale file %r', - temp_output) - if err: - logging.error('finalize(): removal was in response to ' - 'error: %s', str(err)) - raise err - - def get_target_size(self, video): - """Get the size that we will convert to for a given video. - - :returns: (width, height) tuple - """ - return utils.rescale_video((video.width, video.height), - (self.width, self.height), - dont_upsize=self.dont_upsize) - - def process_status_line(self, line): - raise NotImplementedError - -class FFmpegConverterInfo(ConverterInfo): - """Base class for all ffmpeg-based conversions. - - Subclasses must override the parameters attribute and supply it with the - ffmpeg command line for the conversion. parameters can either be a list - of arguments, or a string in which case split() will be called to create - the list. - """ - DURATION_RE = re.compile(r'\W*Duration: (\d\d):(\d\d):(\d\d)\.(\d\d)' - '(, start:.*)?(, bitrate:.*)?') - PROGRESS_RE = re.compile(r'(?:frame=.* fps=.* q=.* )?size=.* time=(.*) ' - 'bitrate=(.*)') - LAST_PROGRESS_RE = re.compile(r'frame=.* fps=.* q=.* Lsize=.* time=(.*) ' - 'bitrate=(.*)') - - extension = None - parameters = None - - def get_executable(self): - return settings.get_ffmpeg_executable_path() - - def get_arguments(self, video, output): - args = ['-i', utils.convert_path_for_subprocess(video.filename), - '-strict', 'experimental'] - args.extend(settings.customize_ffmpeg_parameters( - self.get_parameters(video))) - if not (self.audio_only or video.audio_only): - width, height = self.get_target_size(video) - args.append("-s") - args.append('%ix%i' % (width, height)) - args.extend(self.get_extra_arguments(video, output)) - args.append(self.convert_output_path(output)) - return args - - def convert_output_path(self, output_path): - """Convert our output path so that it can be passed to ffmpeg.""" - # this is a bit tricky, because output_path doesn't exist on windows - # yet, so we can't just call convert_path_for_subprocess(). Instead, - # call convert_path_for_subprocess() on the output directory, and - # assume that the filename only contains safe characters - output_dir = os.path.dirname(output_path) - output_filename = os.path.basename(output_path) - return os.path.join(utils.convert_path_for_subprocess(output_dir), - output_filename) - - def get_extra_arguments(self, video, output): - """Subclasses can override this to add argumenst to the ffmpeg command - line. - """ - return [] - - def get_parameters(self, video): - if self.parameters is None: - raise ValueError("%s: parameters is None" % self) - elif isinstance(self.parameters, basestring): - return self.parameters.split() - else: - return list(self.parameters) - - @staticmethod - def _check_for_errors(line): - if line.startswith('Unknown'): - return line - if line.startswith("Error"): - if not line.startswith("Error while decoding stream"): - return line - - @classmethod - def process_status_line(klass, video, line): - error = klass._check_for_errors(line) - if error: - return {'finished': True, 'error': error} - - match = klass.DURATION_RE.match(line) - if match is not None: - hours, minutes, seconds, centi = [ - int(m) for m in match.groups()[:4]] - return {'duration': hms_to_seconds(hours, minutes, - seconds + 0.01 * centi)} - - match = klass.PROGRESS_RE.match(line) - if match is not None: - t = match.group(1) - if ':' in t: - hours, minutes, seconds = [float(m) for m in t.split(':')[:3]] - return {'progress': hms_to_seconds(hours, minutes, seconds)} - else: - return {'progress': float(t)} - - match = klass.LAST_PROGRESS_RE.match(line) - if match is not None: - return {'finished': True} - -class FFmpegConverterInfo1080p(FFmpegConverterInfo): - def __init__(self, name): - FFmpegConverterInfo.__init__(self, name, 1920, 1080) - -class FFmpegConverterInfo720p(FFmpegConverterInfo): - def __init__(self, name): - FFmpegConverterInfo.__init__(self, name, 1080, 720) - -class FFmpegConverterInfo480p(FFmpegConverterInfo): - def __init__(self, name): - FFmpegConverterInfo.__init__(self, name, 720, 480) - -class ConverterManager(object): - def __init__(self): - self.converters = {} - # converter -> brand reverse map. XXX: this code, really, really sucks - # and not very scalable. - self.brand_rmap = {} - self.brand_map = {} - - def add_converter(self, converter): - self.converters[converter.identifier] = converter - - def startup(self): - self.load_simple_converters() - self.load_converters(resources.converter_scripts()) - - def brand_to_converters(self, brand): - try: - return self.brand_map[brand] - except KeyError: - return None - - def converter_to_brand(self, converter): - try: - return self.brand_rmap[converter] - except KeyError: - return None - - def load_simple_converters(self): - from mvc import basicconverters - for converter in basicconverters.converters: - if isinstance(converter, tuple): - brand, realconverters = converter - for realconverter in realconverters: - self.brand_rmap[realconverter] = brand - self.brand_map.setdefault(brand, []).append(realconverter) - self.add_converter(realconverter) - else: - self.brand_rmap[converter] = None - self.brand_map.setdefault(None, []).append(converter) - self.add_converter(converter) - - def load_converters(self, converters): - for converter_file in converters: - global_dict = {} - execfile(converter_file, global_dict) - if 'converters' in global_dict: - for converter in global_dict['converters']: - if isinstance(converter, tuple): - brand, realconverters = converter - for realconverter in realconverters: - self.brand_rmap[realconverter] = brand - self.brand_map.setdefault(brand, []).append(realconverter) - self.add_converter(realconverter) - else: - self.brand_rmap[converter] = None - self.brand_map.setdefault(None, []).append(converter) - self.add_converter(converter) - logger.info('load_converters: loaded %i from %r', - len(global_dict['converters']), - converter_file) - - def list_converters(self): - return self.converters.values() - - def get_by_id(self, id_): - return self.converters[id_] diff --git a/mvc/errors.py b/mvc/errors.py deleted file mode 100644 index 504948b..0000000 --- a/mvc/errors.py +++ /dev/null @@ -1,89 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -"""``miro.errors`` -- Miro exceptions. -""" - -class ActionUnavailableError(ValueError): - """The action attempted can not be done in the current state.""" - def __init__(self, reason): - self.reason = reason - -class WidgetActionError(ActionUnavailableError): - """The widget is not in the right state to perform the requested action. - This usually is not serious, but if not handled the UI will likely be in an - incorrect state. - """ - -class WidgetDomainError(WidgetActionError): - """The widget element requested is not available at this time. This may be a - temporary condition or a result of permanent changes. - """ - def __init__(self, domain, needle, haystack, details=None): - self.domain = domain - self.needle = needle - self.haystack = haystack - self.details = details - - @property - def reason(self): - reason = "looked for {0} in {2}, but found only {1}".format( - repr(self.needle), repr(self.haystack), self.domain) - if self.details: - reason += ": " + self.details - return reason - -class WidgetRangeError(WidgetDomainError): - """Class to handle neat display of ranges in WidgetDomainErrors. Handlers - should generally catch a parent of this. - """ - def __init__(self, domain, needle, start_range, end_range, details=None): - haystack = "{0} to {1}".format(repr(start_range), repr(end_range)) - WidgetDomainError.__init__(self, domain, needle, haystack, details) - -class WidgetNotReadyError(WidgetActionError): - """The widget is not ready to perfom the action given; this must be a - temporary condition that will be resolved when the widget finishes setting - up. - """ - def __init__(self, waiting_for): - self.waiting_for = waiting_for - - @property - def reason(self): - return "waiting for {0}".format(self.waiting_for) - -class UnexpectedWidgetError(ActionUnavailableError): - """The Spanish Inquisition of widget errors. A widget was asked to do - something, had every reason to do so, yet refused. This should always cause - at least a soft_failure; the UI is now in an incorrect state. - """ - -class WidgetUsageError(UnexpectedWidgetError): - """A widget error that is likely the result of incorrect widget usage.""" diff --git a/mvc/execute.py b/mvc/execute.py deleted file mode 100644 index 893d356..0000000 --- a/mvc/execute.py +++ /dev/null @@ -1,49 +0,0 @@ -"""execute.py -- Run executable programs. - -mvc.execute wraps the standard subprocess module in for MVC. -""" - -import os -import subprocess -import sys - -CalledProcessError = subprocess.CalledProcessError - -def default_popen_args(): - retval = { - 'stdin': open(os.devnull, 'rb'), - 'stdout': subprocess.PIPE, - 'stderr': subprocess.STDOUT, - } - if sys.platform == 'win32': - retval['startupinfo'] = subprocess.STARTUPINFO() - retval['startupinfo'].dwFlags |= subprocess.STARTF_USESHOWWINDOW - return retval - -class Popen(subprocess.Popen): - """subprocess.Popen subclass that adds MVC default behavior. - - By default we: - - Use a /dev/null equivilent for stdin - - Use a pipe for stdout - - Redirect stderr to stdout - - use STARTF_USESHOWWINDOW to not open a console window on win32 - - These are just defaults though, they can be overriden by passing different - values to the constructor - """ - def __init__(self, commandline, **kwargs): - final_args = default_popen_args() - final_args.update(kwargs) - subprocess.Popen.__init__(self, commandline, **final_args) - -def check_output(commandline, **kwargs): - """MVC version of subprocess.check_output. - - This performs the same default behavior as the Popen class. - """ - final_args = default_popen_args() - # check_output doesn't use stdout - del final_args['stdout'] - final_args.update(kwargs) - return subprocess.check_output(commandline, **final_args) diff --git a/mvc/openfiles.py b/mvc/openfiles.py deleted file mode 100644 index ef6710a..0000000 --- a/mvc/openfiles.py +++ /dev/null @@ -1,46 +0,0 @@ -"""openfiles.py -- open files/folders.""" - -import logging -import os -import subprocess -import sys - - -# To open paths we use an OS-specific command. The approach is from: -# http://stackoverflow.com/questions/6631299/python-opening-a-folder-in-explorer-nautilus-mac-thingie - -def check_kde(): - return os.environ.get("KDE_FULL_SESSION", None) != None - -def _open_path_osx(path): - subprocess.call(['open', '--', path]) - -def _open_path_kde(path): - subprocess.call(["kfmclient", "exec", "file://" + path]) # kfmclient is part of konqueror - -def _open_path_gnome(path): - subprocess.call(["gnome-open",'--'. path]) - -def _open_path_windows(path): - subprocess.call(['explorer', path]) - -def _open_path(path): - if sys.platform == 'darwin': - _open_path_osx(path) - elif sys.platform == 'linux2': - if check_kde(): - _open_path_kde(path) - else: - _open_path_gnome(path) - elif sys.platform == 'win32': - _open_path_windows(path) - else: - logging.warn("unknown platform: %s", sys.platform) - -def reveal_folder(path): - """Show a folder in the desktop shell (finder/explorer/nautilous, etc).""" - logging.info("reveal_folder: %s", path) - if os.path.isdir(path): - _open_path(path) - else: - _open_path(os.path.dirname(path)) diff --git a/mvc/osx/__init__.py b/mvc/osx/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/mvc/osx/__init__.py +++ /dev/null diff --git a/mvc/osx/app_main.py b/mvc/osx/app_main.py deleted file mode 100644 index ef52ff6..0000000 --- a/mvc/osx/app_main.py +++ /dev/null @@ -1,12 +0,0 @@ -import os -import sys - -from mvc.osx import autoupdate -from mvc.widgets import app -from mvc.widgets import initialize -from mvc.ui.widgets import Application - -# run the app -autoupdate.initialize() -app.widgetapp = Application() -initialize(app.widgetapp) diff --git a/mvc/osx/autoupdate.py b/mvc/osx/autoupdate.py deleted file mode 100644 index 7b17d47..0000000 --- a/mvc/osx/autoupdate.py +++ /dev/null @@ -1,9 +0,0 @@ -from Foundation import * - -def load_sparkle_framework(): - bundlePath = '%s/Sparkle.framework' % Foundation.NSBundle.mainBundle().privateFrameworksPath() - objc.loadBundle('Sparkle', globals(), bundle_path=bundlePath) - -def initialize(): - load_sparkle_framework() - SUUpdater.sharedUpdater().setAutomaticallyChecksForUpdates_(YES) diff --git a/mvc/qtfaststart/__init__.py b/mvc/qtfaststart/__init__.py deleted file mode 100644 index f985b5c..0000000 --- a/mvc/qtfaststart/__init__.py +++ /dev/null @@ -1 +0,0 @@ -VERSION = "1.6" diff --git a/mvc/qtfaststart/exceptions.py b/mvc/qtfaststart/exceptions.py deleted file mode 100644 index f0767e1..0000000 --- a/mvc/qtfaststart/exceptions.py +++ /dev/null @@ -1,5 +0,0 @@ -class FastStartException(Exception): - """ - Raised when something bad happens during processing. - """ - pass
\ No newline at end of file diff --git a/mvc/qtfaststart/processor.py b/mvc/qtfaststart/processor.py deleted file mode 100755 index df2a900..0000000 --- a/mvc/qtfaststart/processor.py +++ /dev/null @@ -1,215 +0,0 @@ -""" - The guts that actually do the work. This is available here for the - 'qtfaststart' script and for your application's direct use. -""" - -import logging -import os -import struct - -#from StringIO import StringIO -try: - from StringIO import StringIO -except ImportError: - from io import StringIO - -from mvc.qtfaststart.exceptions import FastStartException - -CHUNK_SIZE = 8192 - -log = logging.getLogger("qtfaststart") - -# Older versions of Python require this to be defined -if not hasattr(os, 'SEEK_CUR'): - os.SEEK_CUR = 1 - -def read_atom(datastream): - """ - Read an atom and return a tuple of (size, type) where size is the size - in bytes (including the 8 bytes already read) and type is a "fourcc" - like "ftyp" or "moov". - """ - return struct.unpack(">L4s", datastream.read(8)) - - -def get_index(datastream): - """ - Return an index of top level atoms, their absolute byte-position in the - file and their size in a list: - - index = [ - ("ftyp", 0, 24), - ("moov", 25, 2658), - ("free", 2683, 8), - ... - ] - - The tuple elements will be in the order that they appear in the file. - """ - index = [] - - log.debug("Getting index of top level atoms...") - - # Read atoms until we catch an error - while(datastream): - try: - skip = 8 - atom_size, atom_type = read_atom(datastream) - if atom_size == 1: - atom_size = struct.unpack(">Q", datastream.read(8))[0] - skip = 16 - log.debug("%s: %s" % (atom_type, atom_size)) - except: - break - - index.append((atom_type, datastream.tell() - skip, atom_size)) - - if atom_size == 0: - # Some files may end in mdat with no size set, which generally - # means to seek to the end of the file. We can just stop indexing - # as no more entries will be found! - break - - datastream.seek(atom_size - skip, os.SEEK_CUR) - - # Make sure the atoms we need exist - top_level_atoms = set([item[0] for item in index]) - for key in ["moov", "mdat"]: - if key not in top_level_atoms: - log.error("%s atom not found, is this a valid MOV/MP4 file?" % key) - raise FastStartException() - - return index - - -def find_atoms(size, datastream): - """ - This function is a generator that will yield either "stco" or "co64" - when either atom is found. datastream can be assumed to be 8 bytes - into the stco or co64 atom when the value is yielded. - - It is assumed that datastream will be at the end of the atom after - the value has been yielded and processed. - - size is the number of bytes to the end of the atom in the datastream. - """ - stop = datastream.tell() + size - - while datastream.tell() < stop: - try: - atom_size, atom_type = read_atom(datastream) - except: - log.exception("Error reading next atom!") - raise FastStartException() - - if atom_type in ["trak", "mdia", "minf", "stbl"]: - # Known ancestor atom of stco or co64, search within it! - for atype in find_atoms(atom_size - 8, datastream): - yield atype - elif atom_type in ["stco", "co64"]: - yield atom_type - else: - # Ignore this atom, seek to the end of it. - datastream.seek(atom_size - 8, os.SEEK_CUR) - - -def process(infilename, outfilename, limit=0): - """ - Convert a Quicktime/MP4 file for streaming by moving the metadata to - the front of the file. This method writes a new file. - - If limit is set to something other than zero it will be used as the - number of bytes to write of the atoms following the moov atom. This - is very useful to create a small sample of a file with full headers, - which can then be used in bug reports and such. - """ - datastream = open(infilename, "rb") - - # Get the top level atom index - index = get_index(datastream) - - mdat_pos = 999999 - free_size = 0 - - # Make sure moov occurs AFTER mdat, otherwise no need to run! - for atom, pos, size in index: - # The atoms are guaranteed to exist from get_index above! - if atom == "moov": - moov_pos = pos - moov_size = size - elif atom == "mdat": - mdat_pos = pos - elif atom == "free" and pos < mdat_pos: - # This free atom is before the mdat! - free_size += size - log.info("Removing free atom at %d (%d bytes)" % (pos, size)) - - # Offset to shift positions - offset = moov_size - free_size - - if moov_pos < mdat_pos: - # moov appears to be in the proper place, don't shift by moov size - offset -= moov_size - if not free_size: - # No free atoms and moov is correct, we are done! - log.error("This file appears to already be setup for streaming!") - raise FastStartException() - - # Read and fix moov - datastream.seek(moov_pos) - moov = StringIO(datastream.read(moov_size)) - - # Ignore moov identifier and size, start reading children - moov.seek(8) - - for atom_type in find_atoms(moov_size - 8, moov): - # Read either 32-bit or 64-bit offsets - ctype, csize = atom_type == "stco" and ("L", 4) or ("Q", 8) - - # Get number of entries - version, entry_count = struct.unpack(">2L", moov.read(8)) - - log.info("Patching %s with %d entries" % (atom_type, entry_count)) - - # Read entries - entries = struct.unpack(">" + ctype * entry_count, - moov.read(csize * entry_count)) - - # Patch and write entries - moov.seek(-csize * entry_count, os.SEEK_CUR) - moov.write(struct.pack(">" + ctype * entry_count, - *[entry + offset for entry in entries])) - - log.info("Writing output...") - outfile = open(outfilename, "wb") - - # Write ftype - for atom, pos, size in index: - if atom == "ftyp": - datastream.seek(pos) - outfile.write(datastream.read(size)) - - # Write moov - moov.seek(0) - outfile.write(moov.read()) - - # Write the rest - written = 0 - atoms = [item for item in index if item[0] not in ["ftyp", "moov", "free"]] - for atom, pos, size in atoms: - datastream.seek(pos) - - # Write in chunks to not use too much memory - for x in range(size / CHUNK_SIZE): - outfile.write(datastream.read(CHUNK_SIZE)) - written += CHUNK_SIZE - if limit and written >= limit: - # A limit was set and we've just passed it, stop writing! - break - - if size % CHUNK_SIZE: - outfile.write(datastream.read(size % CHUNK_SIZE)) - written += (size % CHUNK_SIZE) - if limit and written >= limit: - # A limit was set and we've just passed it, stop writing! - break diff --git a/mvc/resources/__init__.py b/mvc/resources/__init__.py deleted file mode 100644 index 005041d..0000000 --- a/mvc/resources/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -import os.path -import glob -import sys - -def image_path(name): - return os.path.join(resources_dir(), 'images', name) - -def converter_scripts(): - return glob.glob(os.path.join(resources_dir(), 'converters', '*.py')) - - -def resources_dir(): - if in_py2exe(): - directory = os.path.join(os.path.dirname(sys.executable), "resources") - else: - directory = os.path.dirname(__file__) - return os.path.abspath(directory) - -def in_py2exe(): - return (hasattr(sys,"frozen") and - sys.frozen in ("windows_exe", "console_exe")) diff --git a/mvc/resources/converters/android.py b/mvc/resources/converters/android.py deleted file mode 100644 index ac2007d..0000000 --- a/mvc/resources/converters/android.py +++ /dev/null @@ -1,61 +0,0 @@ -from mvc.converter import FFmpegConverterInfo -from mvc.basicconverters import MP4 - -class AndroidConversion(FFmpegConverterInfo): - media_type = 'android' - extension = 'mp4' - parameters = ('-acodec aac -ac 2 -ab 160k ' - '-vcodec libx264 -preset slow -profile:v baseline -level 30 ' - '-maxrate 10000000 -bufsize 10000000 -f mp4 -threads 0 ') - simple = MP4 - -y = AndroidConversion('Galaxy Y', 320, 240) -mini = AndroidConversion('Galaxy Mini', 320, 240) -ace = AndroidConversion('Galaxy Ace', 480, 320) -admire = AndroidConversion('Galaxy Admire', 480, 320) -charge = AndroidConversion('Galaxy Charge', 800, 480) -s = AndroidConversion('Galaxy S / SII / S Plus', 800, 480) -siii = AndroidConversion('Galaxy SIII', 1280, 720) -nexus = AndroidConversion('Galaxy Nexus', 1280, 720) -tab = AndroidConversion('Galaxy Tab', 1024, 600) -tab_10 = AndroidConversion('Galaxy Tab 10.1', 1280, 800) -note = AndroidConversion('Galaxy Note', 1280, 800) -note = AndroidConversion('Galaxy Note II', 1920, 1080) -infuse = AndroidConversion('Galaxy Infuse', 1280, 800) -epic = AndroidConversion('Galaxy Epic', 800, 480) - -samsung_devices = ('Samsung', [y, mini, ace, admire, charge, s, siii, nexus, - tab, tab_10, note, infuse, epic]) - -wildfire = AndroidConversion('Wildfire', 320, 240) -desire = AndroidConversion('Desire', 800, 480) -incredible = AndroidConversion('Droid Incredible', 800, 480) -thunderbolt = AndroidConversion('Thunderbolt', 800, 480) -evo = AndroidConversion('Evo 4G', 800, 480) -sensation = AndroidConversion('Sensation', 960, 540) -rezound = AndroidConversion('Rezound', 1280, 720) -onex = AndroidConversion('One X', 1280, 720) - -htc_devices = ('HTC', [wildfire, desire, incredible, thunderbolt, evo, - sensation, rezound, onex]) - -droid = AndroidConversion('Droid', 854, 480) -droid_x2 = AndroidConversion('Droid X2', 1280, 720) -razr = AndroidConversion('RAZR', 960, 540) -xoom = AndroidConversion('XOOM', 1280, 800) - -motorola_devices = ('Motorola', [droid, droid_x2, razr, xoom]) - -zio = AndroidConversion('Zio', 800, 480) - -sanyo_devices = ('Sanyo', [zio]) - -small = AndroidConversion('Small (480x320)', 480, 320) -normal = AndroidConversion('Normal (800x480)', 800, 480) -large720 = AndroidConversion('Large (720p)', 1280, 720) -large1080 = AndroidConversion('Large (1080p)', 1920, 1080) - -more_devices = ('More Devices', [small, normal, large720, large1080]) - -converters = [samsung_devices, htc_devices, motorola_devices, sanyo_devices, - more_devices] diff --git a/mvc/resources/converters/apple.py b/mvc/resources/converters/apple.py deleted file mode 100644 index 20cb76e..0000000 --- a/mvc/resources/converters/apple.py +++ /dev/null @@ -1,28 +0,0 @@ -from mvc.converter import FFmpegConverterInfo -from mvc.basicconverters import MP4 - -class AppleConversion(FFmpegConverterInfo): - media_type = 'apple' - extension = 'mp4' - parameters = ('-acodec aac -ac 2 -ab 160k ' - '-vcodec libx264 -preset slow -profile:v baseline -level 30 ' - '-maxrate 10000000 -bufsize 10000000 -vb 1200k -f mp4 ' - '-threads 0') - simple = MP4 - - -DEFAULT_SIZE = (480, 320) - -ipod = AppleConversion('iPod Nano/Classic', *DEFAULT_SIZE) -ipod_touch = AppleConversion('iPod Touch', 640, 480) -ipod_retina = AppleConversion('iPod Touch 4+', 960, 640) -iphone = AppleConversion('iPhone', 640, 480) -iphone_retina = AppleConversion('iPhone 4+', 960, 640) -iphone_5 = AppleConversion('iPhone 5', 1920, 1080) -ipad = AppleConversion('iPad', 1024, 768) -ipad_retina = AppleConversion('iPad 3', 1920, 1080) -apple_tv = AppleConversion('Apple TV', 1280, 720) -universal = AppleConversion('Apple Universal', 1280, 720) - -converters = [ipod, ipod_touch, ipod_retina, iphone, iphone_retina, iphone_5, - ipad, ipad_retina, apple_tv, universal] diff --git a/mvc/resources/converters/others.py b/mvc/resources/converters/others.py deleted file mode 100644 index a05030f..0000000 --- a/mvc/resources/converters/others.py +++ /dev/null @@ -1,20 +0,0 @@ -from mvc.converter import FFmpegConverterInfo - -class PlaystationPortable(FFmpegConverterInfo): - media_type = 'other' - extension = 'mp4' - parameters = ('-b 512000 -ar 24000 -ab 64000 ' - '-f psp -r 29.97').split() - - -class KindleFire(FFmpegConverterInfo): - media_type = 'other' - extension = 'mp4' - parameters = ('-acodec aac -ab 96k -vcodec libx264 ' - '-preset slow -f mp4 -crf 22').split() - - -psp = PlaystationPortable('Playstation Portable', 320, 240) -kindle_fire = KindleFire('Kindle Fire', 1224, 600) - -converters = [psp, kindle_fire] diff --git a/mvc/resources/images/android-icon-off.png b/mvc/resources/images/android-icon-off.png Binary files differdeleted file mode 100644 index 5948f4c..0000000 --- a/mvc/resources/images/android-icon-off.png +++ /dev/null diff --git a/mvc/resources/images/android-icon-on.png b/mvc/resources/images/android-icon-on.png Binary files differdeleted file mode 100644 index 85be5be..0000000 --- a/mvc/resources/images/android-icon-on.png +++ /dev/null diff --git a/mvc/resources/images/apple-icon-off.png b/mvc/resources/images/apple-icon-off.png Binary files differdeleted file mode 100644 index 947bfae..0000000 --- a/mvc/resources/images/apple-icon-off.png +++ /dev/null diff --git a/mvc/resources/images/apple-icon-on.png b/mvc/resources/images/apple-icon-on.png Binary files differdeleted file mode 100644 index 9949653..0000000 --- a/mvc/resources/images/apple-icon-on.png +++ /dev/null diff --git a/mvc/resources/images/arrow-down-off.png b/mvc/resources/images/arrow-down-off.png Binary files differdeleted file mode 100644 index 368079f..0000000 --- a/mvc/resources/images/arrow-down-off.png +++ /dev/null diff --git a/mvc/resources/images/arrow-down-on.png b/mvc/resources/images/arrow-down-on.png Binary files differdeleted file mode 100644 index 8963b5b..0000000 --- a/mvc/resources/images/arrow-down-on.png +++ /dev/null diff --git a/mvc/resources/images/audio.png b/mvc/resources/images/audio.png Binary files differdeleted file mode 100644 index 4d59605..0000000 --- a/mvc/resources/images/audio.png +++ /dev/null diff --git a/mvc/resources/images/clear-icon.png b/mvc/resources/images/clear-icon.png Binary files differdeleted file mode 100644 index 5b054fa..0000000 --- a/mvc/resources/images/clear-icon.png +++ /dev/null diff --git a/mvc/resources/images/convert-button-off.png b/mvc/resources/images/convert-button-off.png Binary files differdeleted file mode 100644 index 307a8bd..0000000 --- a/mvc/resources/images/convert-button-off.png +++ /dev/null diff --git a/mvc/resources/images/convert-button-on.png b/mvc/resources/images/convert-button-on.png Binary files differdeleted file mode 100644 index 2a66c76..0000000 --- a/mvc/resources/images/convert-button-on.png +++ /dev/null diff --git a/mvc/resources/images/convert-button-stop.png b/mvc/resources/images/convert-button-stop.png Binary files differdeleted file mode 100644 index cb09a97..0000000 --- a/mvc/resources/images/convert-button-stop.png +++ /dev/null diff --git a/mvc/resources/images/converted_to-icon.png b/mvc/resources/images/converted_to-icon.png Binary files differdeleted file mode 100644 index 14ee6d3..0000000 --- a/mvc/resources/images/converted_to-icon.png +++ /dev/null diff --git a/mvc/resources/images/dropoff-icon-off.png b/mvc/resources/images/dropoff-icon-off.png Binary files differdeleted file mode 100644 index e182d49..0000000 --- a/mvc/resources/images/dropoff-icon-off.png +++ /dev/null diff --git a/mvc/resources/images/dropoff-icon-on.png b/mvc/resources/images/dropoff-icon-on.png Binary files differdeleted file mode 100644 index 1dfd88f..0000000 --- a/mvc/resources/images/dropoff-icon-on.png +++ /dev/null diff --git a/mvc/resources/images/dropoff-icon-small-off.png b/mvc/resources/images/dropoff-icon-small-off.png Binary files differdeleted file mode 100644 index 186a7e6..0000000 --- a/mvc/resources/images/dropoff-icon-small-off.png +++ /dev/null diff --git a/mvc/resources/images/dropoff-icon-small-on.png b/mvc/resources/images/dropoff-icon-small-on.png Binary files differdeleted file mode 100644 index 476ea49..0000000 --- a/mvc/resources/images/dropoff-icon-small-on.png +++ /dev/null diff --git a/mvc/resources/images/error-icon.png b/mvc/resources/images/error-icon.png Binary files differdeleted file mode 100644 index 656b2c3..0000000 --- a/mvc/resources/images/error-icon.png +++ /dev/null diff --git a/mvc/resources/images/item-completed.png b/mvc/resources/images/item-completed.png Binary files differdeleted file mode 100644 index 1400eda..0000000 --- a/mvc/resources/images/item-completed.png +++ /dev/null diff --git a/mvc/resources/images/item-delete-button-off.png b/mvc/resources/images/item-delete-button-off.png Binary files differdeleted file mode 100644 index 12cd239..0000000 --- a/mvc/resources/images/item-delete-button-off.png +++ /dev/null diff --git a/mvc/resources/images/item-delete-button-on.png b/mvc/resources/images/item-delete-button-on.png Binary files differdeleted file mode 100644 index 45786e5..0000000 --- a/mvc/resources/images/item-delete-button-on.png +++ /dev/null diff --git a/mvc/resources/images/item-error.png b/mvc/resources/images/item-error.png Binary files differdeleted file mode 100644 index 710ff61..0000000 --- a/mvc/resources/images/item-error.png +++ /dev/null diff --git a/mvc/resources/images/mvc-logo.png b/mvc/resources/images/mvc-logo.png Binary files differdeleted file mode 100644 index fce15e4..0000000 --- a/mvc/resources/images/mvc-logo.png +++ /dev/null diff --git a/mvc/resources/images/other-icon-off.png b/mvc/resources/images/other-icon-off.png Binary files differdeleted file mode 100644 index a6c76f2..0000000 --- a/mvc/resources/images/other-icon-off.png +++ /dev/null diff --git a/mvc/resources/images/other-icon-on.png b/mvc/resources/images/other-icon-on.png Binary files differdeleted file mode 100644 index 6c60edc..0000000 --- a/mvc/resources/images/other-icon-on.png +++ /dev/null diff --git a/mvc/resources/images/progressbar-base.png b/mvc/resources/images/progressbar-base.png Binary files differdeleted file mode 100644 index 298a6b6..0000000 --- a/mvc/resources/images/progressbar-base.png +++ /dev/null diff --git a/mvc/resources/images/queued-icon.png b/mvc/resources/images/queued-icon.png Binary files differdeleted file mode 100644 index d4e9242..0000000 --- a/mvc/resources/images/queued-icon.png +++ /dev/null diff --git a/mvc/resources/images/settings-base_center.png b/mvc/resources/images/settings-base_center.png Binary files differdeleted file mode 100644 index d5f3065..0000000 --- a/mvc/resources/images/settings-base_center.png +++ /dev/null diff --git a/mvc/resources/images/settings-base_left.png b/mvc/resources/images/settings-base_left.png Binary files differdeleted file mode 100644 index a0f10c2..0000000 --- a/mvc/resources/images/settings-base_left.png +++ /dev/null diff --git a/mvc/resources/images/settings-base_right.png b/mvc/resources/images/settings-base_right.png Binary files differdeleted file mode 100644 index 14456eb..0000000 --- a/mvc/resources/images/settings-base_right.png +++ /dev/null diff --git a/mvc/resources/images/settings-depth_center.png b/mvc/resources/images/settings-depth_center.png Binary files differdeleted file mode 100644 index fb5f586..0000000 --- a/mvc/resources/images/settings-depth_center.png +++ /dev/null diff --git a/mvc/resources/images/settings-depth_left.png b/mvc/resources/images/settings-depth_left.png Binary files differdeleted file mode 100644 index a13694b..0000000 --- a/mvc/resources/images/settings-depth_left.png +++ /dev/null diff --git a/mvc/resources/images/settings-depth_right.png b/mvc/resources/images/settings-depth_right.png Binary files differdeleted file mode 100644 index 5ddd21f..0000000 --- a/mvc/resources/images/settings-depth_right.png +++ /dev/null diff --git a/mvc/resources/images/settings-dropdown-bottom-bg.png b/mvc/resources/images/settings-dropdown-bottom-bg.png Binary files differdeleted file mode 100644 index bc650f8..0000000 --- a/mvc/resources/images/settings-dropdown-bottom-bg.png +++ /dev/null diff --git a/mvc/resources/images/settings-icon-off.png b/mvc/resources/images/settings-icon-off.png Binary files differdeleted file mode 100644 index 340b516..0000000 --- a/mvc/resources/images/settings-icon-off.png +++ /dev/null diff --git a/mvc/resources/images/settings-icon-on.png b/mvc/resources/images/settings-icon-on.png Binary files differdeleted file mode 100644 index be008d4..0000000 --- a/mvc/resources/images/settings-icon-on.png +++ /dev/null diff --git a/mvc/resources/images/showfile-icon.png b/mvc/resources/images/showfile-icon.png Binary files differdeleted file mode 100644 index 7f9040f..0000000 --- a/mvc/resources/images/showfile-icon.png +++ /dev/null diff --git a/mvc/resources/nsis/modern-wizard.bmp b/mvc/resources/nsis/modern-wizard.bmp Binary files differdeleted file mode 100644 index d8ea8d9..0000000 --- a/mvc/resources/nsis/modern-wizard.bmp +++ /dev/null diff --git a/mvc/resources/nsis/mvc-logo.ico b/mvc/resources/nsis/mvc-logo.ico Binary files differdeleted file mode 100644 index 007a929..0000000 --- a/mvc/resources/nsis/mvc-logo.ico +++ /dev/null diff --git a/mvc/resources/nsis/plugins/nsProcess.dll b/mvc/resources/nsis/plugins/nsProcess.dll Binary files differdeleted file mode 100644 index 4355d4a..0000000 --- a/mvc/resources/nsis/plugins/nsProcess.dll +++ /dev/null diff --git a/mvc/resources/nsis/plugins/nsProcess.nsh b/mvc/resources/nsis/plugins/nsProcess.nsh deleted file mode 100644 index 76642e0..0000000 --- a/mvc/resources/nsis/plugins/nsProcess.nsh +++ /dev/null @@ -1,21 +0,0 @@ -!define nsProcess::FindProcess `!insertmacro nsProcess::FindProcess`
-
-!macro nsProcess::FindProcess _FILE _ERR
- nsProcess::_FindProcess /NOUNLOAD `${_FILE}`
- Pop ${_ERR}
-!macroend
-
-
-!define nsProcess::KillProcess `!insertmacro nsProcess::KillProcess`
-
-!macro nsProcess::KillProcess _FILE _ERR
- nsProcess::_KillProcess /NOUNLOAD `${_FILE}`
- Pop ${_ERR}
-!macroend
-
-
-!define nsProcess::Unload `!insertmacro nsProcess::Unload`
-
-!macro nsProcess::Unload
- nsProcess::_Unload
-!macroend
diff --git a/mvc/resources/windows/README b/mvc/resources/windows/README deleted file mode 100644 index bcc603e..0000000 --- a/mvc/resources/windows/README +++ /dev/null @@ -1,7 +0,0 @@ -This directory contains resources files for the windows port. - ----- gtkrc --- - -Taken from -http://art.gnome.org/download/themes/gtk2/1203/GTK2-ClearlooksVisto.tar.bz2 -and modified for Libre Video Converter diff --git a/mvc/resources/windows/gtkrc b/mvc/resources/windows/gtkrc deleted file mode 100755 index 45a6969..0000000 --- a/mvc/resources/windows/gtkrc +++ /dev/null @@ -1,182 +0,0 @@ -# Clearlooks-Visto by Marius M. M. < devilx at gdesklets dot org> -# This theme is GPLed :) - -gtk-icon-sizes = "panel-menu=16,16:panel=22,22" - -style "clearlooks-default" -{ - GtkButton::default_border = { 0, 0, 0, 0 } - GtkButton::default_outside_border = { 0, 0, 0, 0 } - GtkRange::trough_border = 0 - - GtkWidget::focus_padding = 1 - - GtkPaned::handle_size = 6 - - GtkRange::slider_width = 15 - GtkRange::stepper_size = 15 - GtkScrollbar::min_slider_length = 30 - GtkCheckButton::indicator_size = 12 - GtkMenuBar::internal-padding = 0 - - GtkTreeView::expander_size = 14 - GtkTreeView::odd_row_color = "#EBF5FF" - GtkExpander::expander_size = 16 - - xthickness = 1 - ythickness = 1 - - fg[NORMAL] = "#505050" - fg[ACTIVE] = "#505050" - fg[SELECTED] = "#ffffff" - fg[INSENSITIVE] = "#9B9B9B" - - bg[NORMAL] = "#F5F5F5" - bg[ACTIVE] = "#f9f9f9" - bg[PRELIGHT] = "#888888" - bg[SELECTED] = "#095fb2" - bg[INSENSITIVE] = "#888888" - - base[NORMAL] = "#ffffff" - base[ACTIVE] = "#095fb2" - base[PRELIGHT] = "#FFFFFF" - base[INSENSITIVE]= "#ffffff" - base[SELECTED] = "#095fb2" - - text[INSENSITIVE]= "#9B9B9B" - text[SELECTED] = "#ffffff" - text[ACTIVE] = "#ffffff" - - engine "clearlooks" - { - contrast = 1.1 - menubarstyle = 2 # 0 = flat, 1 = sunken, 2 = flat gradient - menuitemstyle = 1 # 0 = flat, 1 = 3d-ish (gradient), 2 = 3d-ish (button) - listviewitemstyle = 1 # 0 = flat, 1 = 3d-ish (gradient) - progressbarstyle = 1 # 0 = candy bar, 1 = flat - } -} - - -style "clearlooks-progressbar" = "clearlooks-default" -{ - fg[PRELIGHT] = "#ffffff" - xthickness = 1 - ythickness = 1 - -} - -style "clearlooks-wide" = "clearlooks-default" -{ - xthickness = 2 - ythickness = 2 -} - -style "clearlooks-button" = "clearlooks-default" -{ - xthickness = 3 - ythickness = 3 -} - -style "clearlooks-notebook" = "clearlooks-wide" -{ - bg[NORMAL] = "#FAFAFA" -} - -style "clearlooks-tasklist" = "clearlooks-default" -{ - xthickness = 5 - ythickness = 3 -} - -style "clearlooks-menu" = "clearlooks-default" -{ - xthickness = 2 - ythickness = 1 -} - -style "clearlooks-menubar" = "clearlooks-default" -{ - xthickness = 2 - ythickness = 2 - base[PRELIGHT] = "#63E62E" - base[SELECTED] = "#4DB224" -} - -style "clearlooks-menu-item" = "clearlooks-default" -{ - xthickness = 2 - ythickness = 3 - fg[PRELIGHT] = "#ffffff" - text[PRELIGHT] = "#ffffff" -} - -style "clearlooks-tree" = "clearlooks-default" -{ - xthickness = 2 - ythickness = 2 -} - -style "clearlooks-frame-title" = "clearlooks-default" -{ - fg[NORMAL] = "#505050" -} - -style "clearlooks-panel" = "clearlooks-default" -{ - xthickness = 3 - ythickness = 3 -} - -style "clearlooks-tooltips" = "clearlooks-default" -{ - xthickness = 4 - ythickness = 4 - bg[NORMAL] = { 1.0,1.0,0.75 } -} - -style "clearlooks-combo" = "clearlooks-default" -{ - xthickness = 1 - ythickness = 2 -} - -style "metacity-frame" -{ - bg[SELECTED] = "#095fb2" - fg[SELECTED] = "#ffffff" -} - -class "GtkWidget" style "clearlooks-default" -class "GtkButton" style "clearlooks-button" -class "GtkCombo" style "clearlooks-button" -class "GtkRange" style "clearlooks-wide" -class "GtkFrame" style "clearlooks-wide" -class "GtkMenu" style "clearlooks-menu" -class "GtkEntry" style "clearlooks-button" -class "GtkMenuItem" style "clearlooks-menu-item" -class "GtkStatusbar" style "clearlooks-wide" -class "GtkNotebook" style "clearlooks-notebook" -class "GtkProgressBar" style "clearlooks-progressbar" -class "*MenuBar*" style "clearlooks-menubar" -class "GtkMenuBar*" style "clearlooks-menubar" -class "MetaFrames" style "metacity-frame" - -widget_class "*MenuItem*" style "clearlooks-menu-item" - -widget_class "*.GtkComboBox.GtkButton" style "clearlooks-combo" -widget_class "*.GtkCombo.GtkButton" style "clearlooks-combo" - -widget_class "*.tooltips.*.GtkToggleButton" style "clearlooks-tasklist" -widget "gtk-tooltips" style "clearlooks-tooltips" - -widget_class "*.GtkTreeView.GtkButton" style "clearlooks-tree" -widget_class "*.GtkCTree.GtkButton" style "clearlooks-tree" -widget_class "*.GtkList.GtkButton" style "clearlooks-tree" -widget_class "*.GtkCList.GtkButton" style "clearlooks-tree" -widget_class "*.GtkFrame.GtkLabel" style "clearlooks-frame-title" - -widget_class "*.GtkNotebook.*.GtkEventBox" style "clearlooks-notebook" -widget_class "*.GtkNotebook.*.GtkViewport" style "clearlooks-notebook" - -widget_class "*MenuBar*" style "clearlooks-menubar" diff --git a/mvc/settings.py b/mvc/settings.py deleted file mode 100644 index 4d7255c..0000000 --- a/mvc/settings.py +++ /dev/null @@ -1,88 +0,0 @@ -import logging -import os -import sys - -from mvc import execute - -ffmpeg_version = None - -_search_path_extra = [] -def add_to_search_path(directory): - """Add a path to the list of paths that which() searches.""" - _search_path_extra.append(directory) - -def which(name): - if sys.platform == 'win32': - name = name + '.exe' # we're looking for ffmpeg.exe in this case - if sys.platform == 'darwin' and 'Contents/Resources' in __file__: - # look for a bundled version - path = os.path.join(os.path.dirname(__file__), - '..', '..', '..', '..', 'Helpers', name) - if os.path.exists(path): - return path - dirs_to_search = os.environ['PATH'].split(os.pathsep) - dirs_to_search += _search_path_extra - for dirname in dirs_to_search: - fullpath = os.path.join(dirname, name) - # XXX check for +x bit - if os.path.exists(fullpath): - return fullpath - logging.warn("Can't find path to %s (searched in %s)", name, - dirs_to_search) - -def memoize(func): - cache = [] - def wrapper(): - if not cache: - cache.append(func()) - return cache[0] - return wrapper - -@memoize -def get_ffmpeg_executable_path(): - return which("ffmpeg") - avconv = which('avconv') - if avconv is not None: - return avconv - return which("ffmpeg") - -def get_ffmpeg_version(): - global ffmpeg_version - if ffmpeg_version is None: - commandline = [get_ffmpeg_executable_path(), '-version'] - p = execute.Popen(commandline, stderr=open(os.devnull, "wb")) - stdout, _ = p.communicate() - lines = stdout.split('\n') - version = lines[0].rsplit(' ', 1)[1].split('.') - def maybe_int(v): - try: - return int(v) - except ValueError: - return v - ffmpeg_version = tuple(maybe_int(v) for v in version) - return ffmpeg_version - -def customize_ffmpeg_parameters(params): - """Takes a list of parameters and modifies it based on - platform-specific issues. Returns the newly modified list of - parameters. - - :param params: list of parameters to modify - - :returns: list of modified parameters that will get passed to - ffmpeg - """ - if get_ffmpeg_version() < (0, 8): - # Fallback for older versions of FFmpeg (Ubuntu Natty, in particular). - # see also #18969 - params = ['-vpre' if i == '-preset' else i for i in params] - try: - profile_index = params.index('-profile:v') - except ValueError: - pass - else: - if params[profile_index + 1] == 'baseline': - params[profile_index:profile_index+2] = [ - '-coder', '0', '-bf', '0', '-refs', '1', - '-flags2', '-wpred-dct8x8'] - return params diff --git a/mvc/signals.py b/mvc/signals.py deleted file mode 100644 index 2f64dc9..0000000 --- a/mvc/signals.py +++ /dev/null @@ -1,299 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -"""signals.py - -GObject-like signal handling for Miro. -""" - -import itertools -import logging -import sys -import weakref - -class NestedSignalError(StandardError): - pass - -class WeakMethodReference: - """Used to handle weak references to a method. - - We can't simply keep a weak reference to method itself, because there - almost certainly aren't any other references to it. Instead we keep a - weak reference to the object, it's class and the unbound method. This - gives us enough info to recreate the bound method when we need it. - """ - - def __init__(self, method): - self.object = weakref.ref(method.im_self) - self.func = weakref.ref(method.im_func) - # don't create a weak reference to the class. That only works for - # new-style classes. It's highly unlikely the class will ever need to - # be garbage collected anyways. - self.cls = method.im_class - - def __call__(self): - func = self.func() - if func is None: return None - obj = self.object() - if obj is None: return None - return func.__get__(obj, self.cls) - -class Callback: - def __init__(self, func, extra_args): - self.func = func - self.extra_args = extra_args - - def invoke(self, obj, args): - return self.func(obj, *(args + self.extra_args)) - - def compare_function(self, func): - return self.func == func - - def is_dead(self): - return False - -class WeakCallback: - def __init__(self, method, extra_args): - self.ref = WeakMethodReference(method) - self.extra_args = extra_args - - def compare_function(self, func): - return self.ref() == func - - def invoke(self, obj, args): - callback = self.ref() - if callback is not None: - return callback(obj, *(args + self.extra_args)) - else: - return None - - def is_dead(self): - return self.ref() is None - -class SignalEmitter(object): - def __init__(self, *signal_names): - self.signal_callbacks = {} - self.id_generator = itertools.count() - self._currently_emitting = set() - self._frozen = False - for name in signal_names: - self.create_signal(name) - - def freeze_signals(self): - self._frozen = True - - def thaw_signals(self): - self._frozen = False - - def create_signal(self, name): - self.signal_callbacks[name] = {} - - def get_callbacks(self, signal_name): - try: - return self.signal_callbacks[signal_name] - except KeyError: - raise KeyError("Signal: %s doesn't exist" % signal_name) - - def _check_already_connected(self, name, func): - for callback in self.get_callbacks(name).values(): - if callback.compare_function(func): - raise ValueError("signal %s already connected to %s" % - (name, func)) - - def connect(self, name, func, *extra_args): - """Connect a callback to a signal. Returns an callback handle that - can be passed into disconnect(). - - If func is already connected to the signal, then a ValueError will be - raised. - """ - self._check_already_connected(name, func) - id_ = self.id_generator.next() - callbacks = self.get_callbacks(name) - callbacks[id_] = Callback(func, extra_args) - return (name, id_) - - def connect_weak(self, name, method, *extra_args): - """Connect a callback weakly. Callback must be a method of some - object. We create a weak reference to the method, so that the - connection doesn't keep the object from being garbage collected. - - If method is already connected to the signal, then a ValueError will be - raised. - """ - self._check_already_connected(name, method) - if not hasattr(method, 'im_self'): - raise TypeError("connect_weak must be called with object methods") - id_ = self.id_generator.next() - callbacks = self.get_callbacks(name) - callbacks[id_] = WeakCallback(method, extra_args) - return (name, id_) - - def disconnect(self, callback_handle): - """Disconnect a signal. callback_handle must be the return value from - connect() or connect_weak(). - """ - callbacks = self.get_callbacks(callback_handle[0]) - if callback_handle[1] in callbacks: - del callbacks[callback_handle[1]] - else: - logging.warning( - "disconnect called but callback_handle not in the callback") - - def disconnect_all(self): - for signal in self.signal_callbacks: - self.signal_callbacks[signal] = {} - - def emit(self, name, *args): - if self._frozen: - return - if name in self._currently_emitting: - raise NestedSignalError("Can't emit %s while handling %s" % - (name, name)) - self._currently_emitting.add(name) - try: - callback_returned_true = self._run_signal(name, args) - finally: - self._currently_emitting.discard(name) - self.clear_old_weak_references() - return callback_returned_true - - def _run_signal(self, name, args): - callback_returned_true = False - try: - self_callback = getattr(self, 'do_' + name.replace('-', '_')) - except AttributeError: - pass - else: - if self_callback(*args): - callback_returned_true = True - if not callback_returned_true: - for callback in self.get_callbacks(name).values(): - if callback.invoke(self, args): - callback_returned_true = True - break - return callback_returned_true - - def clear_old_weak_references(self): - for callback_map in self.signal_callbacks.values(): - for id_ in callback_map.keys(): - if callback_map[id_].is_dead(): - del callback_map[id_] - -class SystemSignals(SignalEmitter): - """System wide signals for Miro. These can be accessed from the singleton - object signals.system. Signals include: - - "error" - A problem occurred in Miro. The frontend should let the user - know this happened, hopefully with a nice dialog box or something that - lets the user report the error to bugzilla. - - Arguments: - - report -- string that can be submitted to the bug tracker - - exception -- Exception object (can be None) - - "startup-success" - The startup process is complete. The frontend should - wait for this signal to show the UI to the user. - - No arguments. - - "startup-failure" - The startup process fails. The frontend should inform - the user that this happened and quit. - - Arguments: - - summary -- Short, user-friendly, summary of the problem - - description -- Longer explanation of the problem - - "shutdown" - The backend has shutdown. The event loop is stopped at this - point. - - No arguments. - - "update-available" - A new version of LibreVideoConverter is available. - - Arguments: - - rssItem -- The RSS item for the latest version (in sparkle - appcast format). - - "new-dialog" - The backend wants to display a dialog to the user. - - Arguments: - - dialog -- The dialog to be displayed. - - "theme-first-run" - A theme was used for the first time - - Arguments: - - theme -- The name of the theme. - - "videos-added" -- Videos were added via the singleclick module. - Arguments: - - view -- A database view than contains the videos. - - "download-complete" -- A download was completed. - Arguments: - - item -- an Item of class Item. - - """ - def __init__(self): - SignalEmitter.__init__(self, 'error', 'startup-success', - 'startup-failure', 'shutdown', - 'update-available', 'new-dialog', - 'theme-first-run', 'videos-added', - 'download-complete') - - def shutdown(self): - self.emit('shutdown') - - def update_available(self, latest): - self.emit('update-available', latest) - - def new_dialog(self, dialog): - self.emit('new-dialog', dialog) - - def theme_first_run(self, theme): - self.emit('theme-first-run', theme) - - def videos_added(self, view): - self.emit('videos-added', view) - - def download_complete(self, item): - self.emit('download-complete', item) - - def failed_exn(self, when, details=None): - self.failed(when, with_exception=True, details=details) - - def failed(self, when, with_exception=False, details=None): - """Used to emit the error signal. Formats a nice crash report.""" - if with_exception: - exc_info = sys.exc_info() - else: - exc_info = None - logging.error('%s: %s' % (when, details), exc_info=exc_info) - -system = SystemSignals() diff --git a/mvc/ui/__init__.py b/mvc/ui/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/mvc/ui/__init__.py +++ /dev/null diff --git a/mvc/ui/console.py b/mvc/ui/console.py deleted file mode 100644 index a9751ac..0000000 --- a/mvc/ui/console.py +++ /dev/null @@ -1,120 +0,0 @@ -import json -import operator -import optparse -import time -import sys - -import mvc -from mvc.widgets import app -from mvc.widgets import initialize - -parser = optparse.OptionParser( - usage='%prog [-l] [--list-converters] [-c <converter> <filenames..>]', - version='%prog ' + mvc.VERSION, - prog='python -m mvc.ui.console') -parser.add_option('-j', '--json', action='store_true', - dest='json', - help='Output JSON documents, rather than text.') -parser.add_option('-l', '--list-converters', action='store_true', - dest='list_converters', - help="Print a list of supported converter types.") -parser.add_option('-c', '--converter', dest='converter', - help="Specify the type of conversion to make.") - -class Application(mvc.Application): - - def run(self): - (options, args) = parser.parse_args() - - if options.list_converters: - for c in sorted(self.converter_manager.list_converters(), - key=operator.attrgetter('name')): - if options.json: - print json.dumps({'name': c.name, - 'identifier': c.identifier}) - else: - print '%s (-c %s)' % ( - c.name, - c.identifier) - return - - try: - self.converter_manager.get_by_id(options.converter) - except KeyError: - message = '%r is not a valid converter type.' % ( - options.converter,) - if options.json: - print json.dumps({'error': message}) - else: - print 'ERROR:', message - print 'Use "%s -l" to get a list of valid converters.' % ( - parser.prog,) - print - parser.print_help() - sys.exit(1) - - any_failed = False - - def changed(c): - if c.status == 'failed': - any_failed = True - if options.json: - output = { - 'filename': c.video.filename, - 'output': c.output, - 'status': c.status, - 'duration': c.duration, - 'progress': c.progress, - 'percent': (c.progress_percent * 100 if c.progress_percent - else 0), - } - if c.error is not None: - output['error'] = c.error - print json.dumps(output) - else: - if c.status == 'initialized': - line = 'starting (output: %s)' % (c.output,) - elif c.status == 'converting': - if c.progress_percent is not None: - line = 'converting (%i%% complete, %is remaining)' % ( - c.progress_percent * 100, c.eta) - else: - line = 'converting (0% complete, unknown remaining)' - elif c.status == 'staging': - line = 'staging' - elif c.status == 'failed': - line = 'failed (error: %r)' % (c.error,) - elif c.status == 'finished': - line = 'finished (output: %s)' % (c.output,) - else: - line = c.status - print '%s: %s' % (c.video.filename, line) - - for filename in args: - try: - c = app.start_conversion(filename, options.converter) - except ValueError: - message = 'could not parse %r' % filename - if options.json: - any_failed = True - print json.dumps({'status': 'failed', 'error': message, - 'filename': filename}) - else: - print 'ERROR:', message - continue - changed(c) - c.listen(changed) - - # XXX real mainloop - while self.conversion_manager.running: - self.conversion_manager.check_notifications() - time.sleep(1) - self.conversion_manager.check_notifications() # one last time - - sys.exit(0 if not any_failed else 1) - -if __name__ == "__main__": - initialize(None) - app.widgetapp = Application() - app.widgetapp.startup() - app.widgetapp.run() diff --git a/mvc/ui/widgets.py b/mvc/ui/widgets.py deleted file mode 100644 index 28dabff..0000000 --- a/mvc/ui/widgets.py +++ /dev/null @@ -1,1540 +0,0 @@ -import logging -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - -import os -import sys - -try: - import mvc -except ImportError: - mvc_path = os.path.join(os.path.dirname(__file__), '..', '..') - sys.path.append(mvc_path) - import mvc - -import copy -import tempfile -import urllib -import urlparse - -from mvc.widgets import (initialize, idle_add, mainloop_start, mainloop_stop, - attach_menubar, reveal_file, get_conversion_directory) -from mvc.widgets import menus -from mvc.widgets import widgetset -from mvc.widgets import cellpack -from mvc.widgets import widgetconst -from mvc.widgets import widgetutil -from mvc.widgets import app - -from mvc.converter import ConverterInfo -from mvc.video import VideoFile -from mvc.resources import image_path -from mvc.utils import size_string, round_even, convert_path_for_subprocess -from mvc import openfiles - -BUTTON_FONT = widgetutil.font_scale_from_osx_points(15.0) -LARGE_FONT = widgetutil.font_scale_from_osx_points(13.0) -SMALL_FONT = widgetutil.font_scale_from_osx_points(10.0) - -DEFAULT_FONT="Helvetica" - -CONVERT_TO_FONT = "Gill Sans Light" -CONVERT_TO_FONTSIZE = widgetutil.font_scale_from_osx_points(14.0) - -SETTINGS_FONT = "Gill Sans Light" -SETTINGS_FONTSIZE = widgetutil.font_scale_from_osx_points(13.0) - -CONVERT_NOW_FONT = "Gill Sans Light" -CONVERT_NOW_FONTSIZE = widgetutil.font_scale_from_osx_points(18.0) - -DND_FONT = "Gill Sans Light" -DND_LARGE_FONTSIZE = widgetutil.font_scale_from_osx_points(13.0) -DND_SMALL_FONTSIZE = widgetutil.font_scale_from_osx_points(12.0) - -ITEM_TITLE_FONT = "Futura Medium" -ITEM_TITLE_FONTSIZE = widgetutil.font_scale_from_osx_points(13.0) - -ITEM_ICONS_FONT= "Century Gothic" -ITEM_ICONS_FONTSIZE= widgetutil.font_scale_from_osx_points(10.0) - -GRADIENT_TOP = widgetutil.css_to_color('#585f63') -GRADIENT_BOTTOM = widgetutil.css_to_color('#383d40') - -DRAG_AREA = widgetutil.css_to_color('#2b2e31') - -TEXT_DISABLED = widgetutil.css_to_color('#333333') -TEXT_ACTIVE = widgetutil.css_to_color('#ffffff') -TEXT_CLICKED = widgetutil.css_to_color('#cccccc') -TEXT_INFO = widgetutil.css_to_color('#808080') -TEXT_COLOR = widgetutil.css_to_color('#ffffff') -TEXT_SHADOW = widgetutil.css_to_color('#000000') - -TABLE_WIDTH, TABLE_HEIGHT = 470, 87 - -class CustomLabel(widgetset.Background): - def __init__(self, text=''): - widgetset.Background.__init__(self) - self.text = text - self.font = DEFAULT_FONT - self.font_scale = LARGE_FONT - self.color = TEXT_COLOR - - def set_text(self, text): - self.text = text - self.invalidate_size_request() - - def set_color(self, color): - self.color = color - self.queue_redraw() - - def set_font(self, font, font_scale): - self.font = font - self.font_scale = font_scale - self.invalidate_size_request() - - def textbox(self, layout_manager): - layout_manager.set_text_color(self.color) - layout_manager.set_font(self.font_scale, family=self.font) - font = layout_manager.set_font(self.font_scale, family=self.font) - return layout_manager.textbox(self.text) - - def draw(self, context, layout_manager): - layout_manager.set_text_color(self.color) - layout_manager.set_font(LARGE_FONT, family=self.font) - textbox = self.textbox(layout_manager) - size = textbox.get_size() - textbox.draw(context, 0, (context.height - size[1]) // 2, - context.width, context.height) - - def size_request(self, layout_manager): - return self.textbox(layout_manager).get_size() - -class WebStyleButton(widgetset.CustomButton): - def __init__(self): - super(WebStyleButton, self).__init__() - self.set_cursor(widgetconst.CURSOR_POINTING_HAND) - self.text = '' - self.font = DEFAULT_FONT - self.font_scale = LARGE_FONT - - def set_text(self, text): - self.text = text - self.invalidate_size_request() - - def set_font(self, font, font_scale): - self.font = font - self.font_scale = font_scale - self.invalidate_size_request() - - def textbox(self, layout_manager): - return layout_manager.textbox(self.text, underline=True) - - def size_request(self, layout_manager): - textbox = self.textbox(layout_manager) - return textbox.get_size() - - def draw(self, context, layout_manager): - layout_manager.set_text_color(TEXT_COLOR) - layout_manager.set_font(self.font_scale, family=self.font) - textbox = self.textbox(layout_manager) - size = textbox.get_size() - textbox.draw(context, 0, (context.height - size[1]) // 2, - context.width, context.height) - -class FileDropTarget(widgetset.SolidBackground): - - dropoff_on = widgetset.ImageDisplay(widgetset.Image( - image_path("dropoff-icon-on.png"))) - dropoff_off = widgetset.ImageDisplay(widgetset.Image( - image_path("dropoff-icon-off.png"))) - dropoff_small_on = widgetset.ImageDisplay(widgetset.Image( - image_path("dropoff-icon-small-on.png"))) - dropoff_small_off = widgetset.ImageDisplay(widgetset.Image( - image_path("dropoff-icon-small-off.png"))) - - def __init__(self): - super(FileDropTarget, self).__init__() - self.set_background_color(DRAG_AREA) - self.alignment = widgetset.Alignment( - xscale=0.0, yscale=0.5, - xalign=0.5, yalign=0.5, - top_pad=10, right_pad=40, - bottom_pad=10, left_pad=40) - self.add(self.alignment) - - self.widgets = { - False: self.build_large_widgets(), - True: self.build_small_widgets() - } - - self.normal, self.drag = self.widgets[False] - self.alignment.add(self.normal) - - self.in_drag = False - self.small = False - - def build_large_widgets(self): - height = 40 # arbitrary, but the same for both - normal = widgetset.VBox(spacing=20) - normal.pack_start(widgetutil.align_center(self.dropoff_off, - top_pad=60)) - label = CustomLabel("Drag videos here or") - label.set_color(TEXT_COLOR) - label.set_font(DND_FONT, DND_LARGE_FONTSIZE) - hbox = widgetset.HBox(spacing=4) - hbox.pack_start(widgetutil.align_middle(label)) - - cfb = WebStyleButton() - cfb.set_font(DND_FONT, DND_LARGE_FONTSIZE) - cfb.set_text('Choose Files...') - - cfb.connect('clicked', self.choose_file) - hbox.pack_start(widgetutil.align_middle(cfb)) - hbox.set_size_request(-1, height) - normal.pack_start(hbox) - - drag = widgetset.VBox(spacing=20) - drag.pack_start(widgetutil.align_center(self.dropoff_on, - top_pad=60)) - hbox = widgetset.HBox(spacing=4) - hbox.pack_start(widgetutil.align_center( - widgetset.Label("Release button to drop off", - color=TEXT_COLOR))) - hbox.set_size_request(-1, height) - drag.pack_start(hbox) - return normal, drag - - def build_small_widgets(self): - height = 40 # arbitrary, but the same for both - normal = widgetset.HBox(spacing=4) - normal.pack_start(widgetutil.align_middle(self.dropoff_small_off, - right_pad=7)) - drag_label = CustomLabel('Drag more videos here or') - drag_label.set_font(DND_FONT, DND_SMALL_FONTSIZE) - drag_label.set_color(TEXT_COLOR) - normal.pack_start(widgetutil.align_middle(drag_label)) - cfb = WebStyleButton() - cfb.set_text('Choose Files...') - cfb.set_font(DND_FONT, DND_SMALL_FONTSIZE) - cfb.connect('clicked', self.choose_file) - normal.pack_start(cfb) - normal.set_size_request(-1, height) - - drop_label = CustomLabel('Release button to drop off') - drop_label.set_font(DND_FONT, DND_SMALL_FONTSIZE) - drop_label.set_color(TEXT_COLOR) - drag = widgetset.HBox(spacing=10) - drag.pack_start(widgetutil.align_middle(self.dropoff_small_on)) - drag.pack_start(widgetutil.align_middle(drop_label)) - drag.set_size_request(-1, height) - - return normal, drag - - def set_small(self, small): - if small != self.small: - self.small = small - self.normal, self.drag = self.widgets[small] - self.set_in_drag(self.in_drag, force=True) - - def set_in_drag(self, in_drag, force=False): - if force or in_drag != self.in_drag: - self.in_drag = in_drag - if in_drag: - self.alignment.set_child(self.drag) - else: - self.alignment.set_child(self.normal) - self.queue_redraw() - - def choose_file(self, widget): - app.widgetapp.choose_file() - -BUTTON_BACKGROUND = widgetutil.ThreeImageSurface('settings-base') - -class SettingsButton(widgetset.CustomButton): - - arrow_on = widgetset.ImageSurface(widgetset.Image( - image_path('arrow-down-on.png'))) - arrow_off = widgetset.ImageSurface(widgetset.Image( - image_path('arrow-down-off.png'))) - - def __init__(self, name): - super(SettingsButton, self).__init__() - if name != 'settings': - self.name = name.title() - else: - self.name = None - self.selected = False - if name != 'format': - self.surface_on = widgetset.ImageSurface(widgetset.Image( - image_path('%s-icon-on.png' % name))) - self.surface_off = widgetset.ImageSurface(widgetset.Image( - image_path('%s-icon-off.png' % name))) - if self.surface_on.height != self.surface_off.height: - raise ValueError('invalid surface: height mismatch') - self.image_padding = self.calc_image_padding(name) - else: - self.surface_on = self.surface_off = None - - def calc_image_padding(self, name): - """Add some padding to the bottom of our image icon. This can be used - to fine tune where it gets placed. - - :returns: padding in as a (top, right, bottom, left) tuple - """ - - # NOTE: we vertically center the images, so in order to move it X - # pickels up, we need X*2 pixels of bottom padding - if name == 'android': - return (0, 0, 2, 0) - elif name in ('apple', 'other'): - return (0, 0, 4, 0) - else: - return (0, 0, 0, 0) - - def textbox(self, layout_manager): - layout_manager.set_font(SETTINGS_FONTSIZE, family=SETTINGS_FONT) - return layout_manager.textbox(self.name) - - def size_request(self, layout_manager): - hbox = self.build_hbox(layout_manager) - size = hbox.get_size() - height = max(BUTTON_BACKGROUND.height, size[1]) - return int(size[0]) + 2, int(height) + 2 # padding - - def build_hbox(self, layout_manager): - hbox = cellpack.HBox(spacing=5) - if self.selected: - image = self.surface_on - arrow = self.arrow_on - layout_manager.set_text_color(TEXT_ACTIVE) - else: - image = self.surface_off - arrow = self.arrow_off - layout_manager.set_text_color(TEXT_DISABLED) - if image: - padding = cellpack.Padding(image, *self.image_padding) - hbox.pack(cellpack.Alignment(padding, xscale=0, yscale=0, - yalign=0.5)) - if self.name: - vbox = cellpack.VBox() - textbox = self.textbox(layout_manager) - vbox.pack(textbox) - vbox.pack_space(1) - hbox.pack(cellpack.Alignment(vbox, yscale=0, yalign=0.5), - expand=True) - a = cellpack.Alignment(arrow, xscale=0, yscale=0, yalign=0.5) - hbox.pack(cellpack.Padding(a, left=5, right=12)) - alignment = cellpack.Padding(hbox, left=5) - return alignment - - def draw(self, context, layout_manager): - BUTTON_BACKGROUND.draw(context, 1, 1, context.width - 2) - alignment = self.build_hbox(layout_manager) - padding = cellpack.Padding(alignment, top=1, right=3, bottom=1, left=3) - padding.render_layout(context) - - def set_selected(self, selected): - self.selected = selected - self.queue_redraw() - - -class OptionMenuBackground(widgetset.Background): - def __init__(self): - widgetset.Background.__init__(self) - self.surface = widgetutil.ThreeImageSurface('settings-depth') - - def set_child(self, child): - widgetset.Background.set_child(self, child) - # re-create the image surface and scale it as it needs to cover - # the whole of the height of the child - _, h = child.get_size_request() - self.surface = widgetutil.ThreeImageSurface('settings-depth', height=h) - self.invalidate_size_request() - - def size_request(self, layout_manager): - return -1, self.surface.height - - def draw(self, context, layout_manager): - child_width = self.child.get_size_request()[0] - self.surface.draw(context, 0, 0, child_width) - - -class BottomBackground(widgetset.Background): - - def draw(self, context, layout_manager): - gradient = widgetset.Gradient(0, 0, 0, context.height) - gradient.set_start_color(GRADIENT_TOP) - gradient.set_end_color(GRADIENT_BOTTOM) - context.rectangle(0, 0, context.width, context.height) - context.gradient_fill(gradient) - - -class LabeledNumberEntry(widgetset.HBox): - - def __init__(self, label): - super(LabeledNumberEntry, self).__init__(spacing=5) - self.label = widgetset.Label(label, color=TEXT_COLOR) - self.label.set_size(widgetconst.SIZE_SMALL) - self.entry = widgetset.NumberEntry() - self.entry.set_size_request(50, 20) - self.pack_start(self.label) - self.pack_start(self.entry) - self.entry.connect('focus-out', lambda x: self.emit('focus-out')) - - def get_text(self): - return self.entry.get_text() - - def set_text(self, text): - self.entry.set_text(text) - - def get_value(self): - try: - return int(self.entry.get_text()) - except ValueError: - return None - - -class CustomOptions(widgetset.Background): - - background = widgetset.ImageSurface(widgetset.Image( - image_path('settings-dropdown-bottom-bg.png'))) - - def __init__(self): - super(CustomOptions, self).__init__() - self.create_signal('setting-changed') - self.reset() - - def reset(self): - self.options = { - 'destination': None, - 'custom-size': False, - 'width': None, - 'height': None, - 'custom-aspect': False, - 'aspect-ratio': 4.0/3.0, - 'dont-upsize': True - } - - self.top = self.create_top() - self.top.set_size_request(390, 50) - self.left = self.create_left() - self.left.set_size_request(212, 70) - self.right = self.create_right() - self.right.set_size_request(178, 70) - vbox = widgetset.VBox() - vbox.pack_start(self.top) - hbox = widgetset.HBox() - hbox.pack_start(self.left) - hbox.pack_start(self.right) - vbox.pack_start(hbox) - - self.box = widgetutil.align_left(vbox) - - if self.child: - self.set_child(self.box) - - def create_top(self): - hbox = widgetset.HBox(spacing=0) - path_label = WebStyleButton() - path_label.set_text('Show output folder') - path_label.set_font(DEFAULT_FONT, widgetconst.SIZE_SMALL) - path_label.connect('clicked', self.on_path_label_clicked) - create_thumbnails = widgetset.Checkbox('Create Thumbnails', - color=TEXT_COLOR) - create_thumbnails.set_size(widgetconst.SIZE_SMALL) - create_thumbnails.connect('toggled', - self.on_create_thumbnails_changed) - - hbox.pack_start(widgetutil.align(path_label, xalign=0.5), expand=True) - hbox.pack_start(widgetutil.align(create_thumbnails, xalign=0.5), - expand=True) - # XXX: disabled until we can figure out how to do this properly. - #button = widgetset.Button('...') - #button.connect('clicked', self.on_destination_clicked) - #reset = widgetset.Button('Reset') - #reset.connect('clicked', self.on_destination_reset) - #hbox.pack_start(button) - #hbox.pack_start(reset) - return widgetutil.align(hbox, xscale=1.0, yalign=0.5) - - def _get_save_to_path(self): - if self.options['destination'] is None: - return get_conversion_directory() - else: - return self.options['destination'] - - def on_path_label_clicked(self, label): - save_path = self._get_save_to_path() - save_path = convert_path_for_subprocess(save_path) - openfiles.reveal_folder(save_path) - - def create_left(self): - self.custom_size = widgetset.Checkbox('Custom Size', color=TEXT_COLOR) - self.custom_size.set_size(widgetconst.SIZE_SMALL) - self.custom_size.connect('toggled', self.on_custom_size_changed) - - dont_upsize = widgetset.Checkbox('Don\'t Upsize', color=TEXT_COLOR) - dont_upsize.set_checked(self.options['dont-upsize']) - dont_upsize.set_size(widgetconst.SIZE_SMALL) - dont_upsize.connect('toggled', self.on_dont_upsize_changed) - - bottom = widgetset.HBox(spacing=5) - self.width_widget = LabeledNumberEntry('Width') - self.width_widget.connect('focus-out', self.on_width_changed) - self.width_widget.entry.connect('activate', - self.on_width_changed) - self.width_widget.disable() - self.height_widget = LabeledNumberEntry('Height') - self.height_widget.connect('focus-out', self.on_height_changed) - self.height_widget.entry.connect('activate', - self.on_height_changed) - self.height_widget.disable() - bottom.pack_start(self.width_widget) - bottom.pack_start(self.height_widget) - - hbox = widgetset.HBox(spacing=5) - hbox.pack_start(self.custom_size) - hbox.pack_start(dont_upsize) - - vbox = widgetset.VBox(spacing=5) - vbox.pack_start(widgetutil.align_left(hbox, left_pad=10)) - vbox.pack_start(widgetutil.align_center(bottom)) - return widgetutil.align_middle(vbox) - - def create_right(self): - aspect = widgetset.Checkbox('Custom Aspect Ratio', color=TEXT_COLOR) - aspect.set_size(widgetconst.SIZE_SMALL) - aspect.connect('toggled', self.on_aspect_changed) - self.aspect_widget = aspect - self.button_group = widgetset.RadioButtonGroup() - b1 = widgetset.RadioButton('4:3', self.button_group, color=TEXT_COLOR) - b2 = widgetset.RadioButton('3:2', self.button_group, color=TEXT_COLOR) - b3 = widgetset.RadioButton('16:9', self.button_group, color=TEXT_COLOR) - b1.set_selected() - b1.set_size(widgetconst.SIZE_SMALL) - b2.set_size(widgetconst.SIZE_SMALL) - b3.set_size(widgetconst.SIZE_SMALL) - self.aspect_map = dict() - self.aspect_map[b1] = (4, 3) - self.aspect_map[b2] = (3, 2) - self.aspect_map[b3] = (16, 9) - hbox = widgetset.HBox(spacing=5) - # Because the custom size starts off as disabled, so should aspect - # ratio as aspect ratio is dependent on a custom size set. - self.aspect_widget.disable() - for button in self.button_group.get_buttons(): - button.disable() - button.set_size(widgetconst.SIZE_SMALL) - hbox.pack_start(button) - button.connect('clicked', self.on_aspect_size_changed) - - vbox = widgetset.VBox() - vbox.pack_start(widgetutil.align_center(aspect)) - vbox.pack_start(widgetutil.align_center(hbox)) - return widgetutil.align_middle(vbox) - - def draw(self, context, layout_manager): - self.background.draw(context, 0, 0, self.background.width, - self.background.height) - - def enable_custom_size(self): - self.custom_size.enable() - - def disable_custom_size(self): - self.custom_size.disable() - self.custom_size.set_checked(False) - - def update_setting(self, setting, value): - self.options[setting] = value - if setting in ('width', 'height'): - if value is not None: - widget_text = str(value) - else: - widget_text = '' - if setting == 'width': - self.width_widget.set_text(widget_text) - elif setting == 'height': - self.height_widget.set_text(widget_text) - - def do_setting_changed(self, setting, value): - logging.info('setting-changed: %s -> %s', setting, value) - - def _change_setting(self, setting, value): - """Handles setting changes in response to widget changes.""" - - self.options[setting] = value - self.emit('setting-changed', setting, value) - - def force_width_to_aspect_ratio(self): - aspect_ratio = self.options['aspect-ratio'] - width = self.width_widget.get_text() - height = self.height_widget.get_text() - if not height: - return - new_width = round_even(float(height) * aspect_ratio) - if new_width != width: - self.update_setting('width', new_width) - self.emit('setting-changed', 'width', new_width) - - def force_height_to_aspect_ratio(self): - aspect_ratio = self.options['aspect-ratio'] - width = self.width_widget.get_text() - height = self.height_widget.get_text() - if not width: - return - new_height = round_even(float(width) / aspect_ratio) - if new_height != height: - self.update_setting('height', new_height) - self.emit('setting-changed', 'height', new_height) - - def show(self): - self.set_child(self.box) - self.set_size_request(self.background.width, - self.background.height + 28) - self.queue_redraw() - - def hide(self): - self.remove() - self.set_size_request(0, 0) - self.queue_redraw() - - def toggle(self): - if self.child: - self.hide() - else: - self.show() - - # signal handlers - def on_destination_clicked(self, widget): - dialog = widgetset.DirectorySelectDialog('Destination Directory') - r = dialog.run() - if r == 0: # picked a directory - self._change_setting('destination', directory) - - def on_destination_reset(self, widget): - self._change_setting('destination', None) - - def on_dont_upsize_changed(self, widget): - self._change_setting('dont-upsize', widget.get_checked()) - - def on_custom_size_changed(self, widget): - self._change_setting('custom-size', widget.get_checked()) - if widget.get_checked(): - self.width_widget.enable() - self.height_widget.enable() - self.aspect_widget.enable() - self.on_aspect_changed(self.aspect_widget) - else: - self.width_widget.disable() - self.height_widget.disable() - self.aspect_widget.disable() - self.on_aspect_changed(self.aspect_widget) - for button in self.button_group.get_buttons(): - button.disable() - - def on_create_thumbnails_changed(self, widget): - self._change_setting('create-thumbnails', widget.get_checked()) - - def on_width_changed(self, widget): - self._change_setting('width', self.width_widget.get_value()) - if self.options['custom-aspect']: - self.force_height_to_aspect_ratio() - - def on_height_changed(self, widget): - self._change_setting('height', self.height_widget.get_value()) - if self.options['custom-aspect']: - self.force_width_to_aspect_ratio() - - def on_aspect_changed(self, widget): - self._change_setting('custom-aspect', widget.get_checked()) - if widget.get_checked(): - self.force_height_to_aspect_ratio() - for button in self.button_group.get_buttons(): - button.enable() - else: - for button in self.button_group.get_buttons(): - button.disable() - - def on_aspect_size_changed(self, widget): - if self.options['custom-aspect']: - width_ratio, height_ratio = [float(v) for v in - self.aspect_map[widget]] - ratio = width_ratio / height_ratio - self._change_setting('aspect-ratio', ratio) - self.force_height_to_aspect_ratio() - -EMPTY_CONVERTER = ConverterInfo("") - - -class ConversionModel(widgetset.TableModel): - def __init__(self): - super(ConversionModel, self).__init__( - 'text', # filename - 'numeric', # output_size - 'text', # converter - 'text', # status - 'numeric', # duration - 'numeric', # progress - 'numeric', # eta, - 'object', # image - 'object', # the actual conversion - ) - self.conversion_to_iter = {} - self.thumbnail_to_image = {None: widgetset.Image( - image_path('audio.png'))} - - def conversions(self): - return iter(self.conversion_to_iter) - - def all_conversions_done(self): - has_conversions = any(self.conversions()) - all_done = ((set(c.status for c in self.conversions()) - - set(['canceled', 'finished', 'failed'])) == set()) - return all_done and has_conversions - - def get_image(self, path): - if path not in self.thumbnail_to_image: - try: - image = widgetset.Image(path) - except ValueError: - image = self.thumbnail_to_image[None] - self.thumbnail_to_image[path] = image - return self.thumbnail_to_image[path] - - def update_conversion(self, conversion): - try: - output_size = os.stat(conversion.output).st_size - except OSError: - output_size = 0 - - def complete(): - # needs to do it on the update_conversion() from app object - # which calls model_changed() and redraws for us - app.widgetapp.update_conversion(conversion) - - values = (conversion.video.filename, - output_size, - conversion.converter.name, - conversion.status, - conversion.duration or 0, - conversion.progress or 0, - conversion.eta or 0, - self.get_image(conversion.video.get_thumbnail(complete, 90, 70)), - conversion - ) - iter_ = self.conversion_to_iter.get(conversion) - if iter_ is None: - self.conversion_to_iter[conversion] = self.append(*values) - else: - self.update(iter_, *values) - - def remove(self, iter_): - conversion = self[iter_][-1] - del self.conversion_to_iter[conversion] - - # XXX If we add/remove too quickly, we could still be processing - # thumbnails and this may return null, and the self.thumbnail_to_image - # dictionary may get out of sync - def complete(path): - logging.info('calling completion handler for get_thumbnail on ' - 'removal') - - thumbnail_path = conversion.video.get_thumbnail(complete, 90, 70) - if thumbnail_path: - del self.thumbnail_to_image[thumbnail_path] - return super(ConversionModel, self).remove(iter_) - - -class IconWithText(cellpack.HBox): - - def __init__(self, icon, textbox): - super(IconWithText, self).__init__(spacing=5) - self.pack(cellpack.Alignment(icon, yalign=0.5, xscale=0, yscale=0)) - self.pack(textbox) - - -class ConversionCellRenderer(widgetset.CustomCellRenderer): - - IGNORE_PADDING = True - - clear = widgetset.ImageSurface(widgetset.Image( - image_path("clear-icon.png"))) - converted_to = widgetset.ImageSurface(widgetset.Image( - image_path("converted_to-icon.png"))) - queued = widgetset.ImageSurface(widgetset.Image( - image_path("queued-icon.png"))) - showfile = widgetset.ImageSurface(widgetset.Image( - image_path("showfile-icon.png"))) - show_ffmpeg = widgetset.ImageSurface(widgetset.Image( - image_path("error-icon.png"))) - progressbar_base = widgetset.ImageSurface(widgetset.Image( - image_path("progressbar-base.png"))) - delete_on = widgetset.ImageSurface(widgetset.Image( - image_path("item-delete-button-on.png"))) - delete_off = widgetset.ImageSurface(widgetset.Image( - image_path("item-delete-button-off.png"))) - error = widgetset.ImageSurface(widgetset.Image( - image_path("item-error.png"))) - completed = widgetset.ImageSurface(widgetset.Image( - image_path("item-completed.png"))) - - def __init__(self): - super(ConversionCellRenderer, self).__init__() - self.alignment = None - - def get_size(self, style, layout_manager): - return TABLE_WIDTH, TABLE_HEIGHT - - def render(self, context, layout_manager, selected, hotspot, hover): - left_right = cellpack.HBox() - top_bottom = cellpack.VBox() - left_right.pack(self.layout_left(layout_manager)) - left_right.pack(top_bottom, expand=True) - layout_manager.set_text_color(TEXT_COLOR) - layout_manager.set_font(ITEM_TITLE_FONTSIZE, bold=True, - family=ITEM_TITLE_FONT) - title = layout_manager.textbox(os.path.basename(self.input)) - title.set_wrap_style('truncated-char') - alignment = cellpack.Padding(cellpack.TruncatedTextLine(title), - top=25) - top_bottom.pack(alignment) - layout_manager.set_font(ITEM_ICONS_FONTSIZE, family=ITEM_ICONS_FONT) - - bottom = self.layout_bottom(layout_manager, hotspot) - if bottom is not None: - top_bottom.pack(bottom) - left_right.pack(self.layout_right(layout_manager, hotspot)) - - alignment = cellpack.Alignment(left_right, yscale=0, yalign=0.5) - self.alignment = alignment - - background = cellpack.Background(alignment) - background.set_callback(self.draw_background) - background.render_layout(context) - - @staticmethod - def draw_background(context, x, y, width, height): - # draw main background - gradient = widgetset.Gradient(x, y, x, height) - gradient.set_start_color(GRADIENT_TOP) - gradient.set_end_color(GRADIENT_BOTTOM) - context.rectangle(x, y, width, height) - context.gradient_fill(gradient) - # draw bottom line - context.set_line_width(1) - context.set_color((0, 0, 0)) - context.move_to(0, height-0.5) - context.line_to(context.width, height-0.5) - context.stroke() - - def draw_progressbar(self, context, x, y, _, height, width): - # We're only drawing a certain amount of width, not however much we're - # allocated. So, we ignore the passed-in width and just use what we - # set in layout_bottom. - widgetutil.circular_rect(context, x, y, width-1, height-1) - context.set_color((1, 1, 1)) - context.fill() - - def layout_left(self, layout_manager): - surface = widgetset.ImageSurface(self.thumbnail) - return cellpack.Padding(surface, 10, 10, 10, 10) - - def layout_right(self, layout_manager, hotspot): - alignment_kwargs = dict( - xalign=0.5, - xscale=0, - yalign=0.5, - yscale=0, - min_width=80) - if self.status == 'finished': - return cellpack.Alignment(self.completed, **alignment_kwargs) - elif self.status in ('canceled', 'failed'): - return cellpack.Alignment(self.error, **alignment_kwargs) - else: - if hotspot == 'cancel': - image = self.delete_on - else: - image = self.delete_off - return cellpack.Alignment(cellpack.Hotspot('cancel', - image), - **alignment_kwargs) - - def layout_bottom(self, layout_manager, hotspot): - layout_manager.set_text_color(TEXT_COLOR) - if self.status in ('converting', 'staging'): - box = cellpack.HBox(spacing=5) - stack = cellpack.Stack() - stack.pack(cellpack.Alignment(self.progressbar_base, - yalign=0.5, - xscale=0, yscale=0)) - percent = self.progress / self.duration - width = max(int(percent * self.progressbar_base.width), - 5) - stack.pack(cellpack.DrawingArea( - width, self.progressbar_base.height, - self.draw_progressbar, width)) - box.pack(cellpack.Alignment(stack, - yalign=0.5, - xscale=0, yscale=0)) - textbox = layout_manager.textbox("%d%%" % ( - 100 * percent)) - box.pack(textbox) - return box - elif self.status == 'initialized': # queued - vbox = cellpack.VBox() - vbox.pack_space(2) - vbox.pack(IconWithText(self.queued, - layout_manager.textbox("Queued"))) - return vbox - elif self.status in ('finished', 'failed', 'canceled'): - vbox = cellpack.VBox(spacing=5) - vbox.pack_space(4) - top = cellpack.HBox(spacing=5) - if self.status == 'finished': - if hotspot == 'show-file': - layout_manager.set_text_color(TEXT_CLICKED) - top.pack(cellpack.Hotspot('show-file', IconWithText( - self.showfile, - layout_manager.textbox('Show File', - underline=True)))) - elif self.status in ('failed', 'canceled'): - color = TEXT_CLICKED if hotspot == 'show-log' else TEXT_COLOR - layout_manager.set_text_color(color) - # XXX Missing grey error icon - if self.status == 'failed': - text = 'Error - Show FFmpeg Output' - else: - text = 'Canceled - Show FFmpeg Output' - top.pack(cellpack.Hotspot('show-log', IconWithText( - self.show_ffmpeg, - layout_manager.textbox(text, underline=True)))) - color = TEXT_CLICKED if hotspot == 'clear' else TEXT_COLOR - layout_manager.set_text_color(color) - top.pack(cellpack.Hotspot('clear', IconWithText( - self.showfile, - layout_manager.textbox('Clear', underline=True)))) - vbox.pack(top) - if self.status == 'finished': - layout_manager.set_text_color(TEXT_INFO) - vbox.pack(IconWithText( - self.converted_to, - layout_manager.textbox("Converted to %s" % ( - size_string(self.output_size))))) - return vbox - - def hotspot_test(self, style, layout_manager, x, y, width, height): - if self.alignment is None: - return - hotspot_info = self.alignment.find_hotspot(x, y, width, height) - if hotspot_info: - return hotspot_info[0] - -class ConvertButton(widgetset.CustomButton): - off = widgetset.ImageSurface(widgetset.Image( - image_path("convert-button-off.png"))) - clear = widgetset.ImageSurface(widgetset.Image( - image_path("convert-button-off.png"))) - on = widgetset.ImageSurface(widgetset.Image( - image_path("convert-button-on.png"))) - stop = widgetset.ImageSurface(widgetset.Image( - image_path("convert-button-stop.png"))) - - def __init__(self): - super(ConvertButton, self).__init__() - self.hidden = False - self.set_off() - - def set_on(self): - self.label = 'Convert to %s' % app.widgetapp.current_converter.name - self.image = self.on - self.set_cursor(widgetconst.CURSOR_POINTING_HAND) - self.queue_redraw() - - def set_clear(self): - self.label = 'Clear and Start Over' - self.image = self.clear - self.set_cursor(widgetconst.CURSOR_POINTING_HAND) - self.queue_redraw() - - def set_off(self): - self.label = 'Convert Now' - self.image = self.off - self.set_cursor(widgetconst.CURSOR_NORMAL) - self.queue_redraw() - - def set_stop(self): - self.label = 'Stop All Conversions' - self.image = self.stop - self.set_cursor(widgetconst.CURSOR_POINTING_HAND) - self.queue_redraw() - - def hide(self): - self.hidden = True - self.invalidate_size_request() - self.queue_redraw() - - def show(self): - self.hidden = False - self.invalidate_size_request() - self.queue_redraw() - - def size_request(self, layout_manager): - if self.hidden: - return 0, 0 - return self.off.width, self.off.height - - def draw(self, context, layout_manager): - if self.hidden: - return - self.image.draw(context, 0, 0, self.image.width, self.image.height) - layout_manager.set_font(CONVERT_NOW_FONTSIZE, family=CONVERT_NOW_FONT) - if self.image == self.off: - layout_manager.set_text_shadow(widgetutil.Shadow(TEXT_SHADOW, - 0.5, (-1, -1), 0)) - layout_manager.set_text_color(TEXT_DISABLED) - else: - layout_manager.set_text_shadow(widgetutil.Shadow(TEXT_SHADOW, - 0.5, (1, 1), 0)) - layout_manager.set_text_color(TEXT_ACTIVE) - textbox = layout_manager.textbox(self.label) - alignment = cellpack.Alignment(textbox, xalign=0.5, xscale=0.0, - yalign=0.5, yscale=0) - alignment.render_layout(context) - -# XXX do we want to export this for general purpose use? -class TextDialog(widgetset.Dialog): - def __init__(self, title, description, window): - widgetset.Dialog.__init__(self, title, description) - self.set_transient_for(window) - self.add_button('OK') - self.textbox = widgetset.MultilineTextEntry() - self.textbox.set_editable(False) - scroller = widgetset.Scroller(False, True) - scroller.set_has_borders(True) - scroller.add(self.textbox) - scroller.set_size_request(400, 500) - self.set_extra_widget(scroller) - - def set_text(self, text): - self.textbox.set_text(text) - -class Application(mvc.Application): - def __init__(self, simultaneous=None): - mvc.Application.__init__(self, simultaneous) - self.create_signal('window-shown') - self.sent_window_shown = False - - def startup(self): - if self.started: - return - - self.current_converter = EMPTY_CONVERTER - - mvc.Application.startup(self) - - self.menu_manager = menus.MenuManager() - self.menu_manager.setup_menubar(self.menubar) - - self.window = widgetset.Window("Libre Video Converter") - self.window.connect('on-shown', self.on_window_shown) - self.window.connect('will-close', self.destroy) - - # # table on top - self.model = ConversionModel() - self.table = widgetset.TableView(self.model) - self.table.draws_selection = False - self.table.set_row_spacing(0) - self.table.enable_album_view_focus_hack() - self.table.set_fixed_height(True) - self.table.set_grid_lines(False, False) - self.table.set_show_headers(False) - - c = widgetset.TableColumn("Data", ConversionCellRenderer(), - **dict((n, v) for (v, n) in enumerate(( - 'input', 'output_size', 'converter', 'status', - 'duration', 'progress', 'eta', 'thumbnail', - 'conversion')))) - c.set_min_width(TABLE_WIDTH) - self.table.add_column(c) - self.table.connect('hotspot-clicked', self.hotspot_clicked) - - # bottom buttons - converter_types = ('apple', 'android', 'other', 'format') - converters = {} - for c in self.converter_manager.list_converters(): - media_type = c.media_type - if media_type not in converter_types: - media_type = 'others' - brand = self.converter_manager.converter_to_brand(c) - # None = top level. Otherwise tack on the brand name. - if brand is None: - converters.setdefault(media_type, set()).add(c) - else: - converters.setdefault(media_type, set()).add(brand) - - self.menus = [] - - self.button_bar = widgetset.HBox() - buttons = widgetset.HBox() - - for type_ in converter_types: - options = [] - more_devices = None - for c in converters[type_]: - if isinstance(c, str): - rconverters = self.converter_manager.brand_to_converters(c) - values = [] - for r in rconverters: - values.append((r.name, r.identifier)) - # yuck - if c == 'More Devices': - more_devices = (c, values) - else: - options.append((c, values)) - else: - options.append((c.name, c.identifier)) - # Don't sort if formats.. - self.sort_converter_menu(type_, options) - if more_devices: - options.append(more_devices) - menu = SettingsButton(type_) - menu.connect('clicked', self.show_options_menu, options) - self.menus.append(menu) - buttons.pack_start(menu) - omb = OptionMenuBackground() - omb.set_child(widgetutil.pad(buttons, top=2, bottom=2, - left=2, right=2)) - self.button_bar.pack_start(omb) - - self.settings_button = SettingsButton('settings') - omb = OptionMenuBackground() - omb.set_child(widgetutil.pad(self.settings_button, top=2, - bottom=2, left=2, right=2)) - self.button_bar.pack_end(omb) - - self.drop_target = FileDropTarget() - self.drop_target.set_size_request(-1, 70) - - # # finish up - vbox = widgetset.VBox() - self.vbox = vbox - - # add menubars, if we're not on windows - if sys.platform != 'win32': - attach_menubar() - - self.scroller = widgetset.Scroller(False, True) - self.scroller.set_size_request(0, 0) - self.scroller.set_background_color(DRAG_AREA) - self.scroller.add(self.table) - vbox.pack_start(self.scroller) - vbox.pack_start(self.drop_target, expand=True) - - bottom = BottomBackground() - bottom_box = widgetset.VBox() - self.convert_label = CustomLabel('Convert to') - self.convert_label.set_font(CONVERT_TO_FONT, CONVERT_TO_FONTSIZE) - self.convert_label.set_color(TEXT_COLOR) - bottom_box.pack_start(widgetutil.align_left(self.convert_label, - top_pad=10, - bottom_pad=10)) - bottom_box.pack_start(self.button_bar) - - self.options = CustomOptions() - self.options.connect('setting-changed', self.on_setting_changed) - self.settings_button.connect('clicked', self.on_settings_toggle) - bottom_box.pack_start(widgetutil.align_right(self.options, - right_pad=5)) - - self.convert_button = ConvertButton() - self.convert_button.connect('clicked', self.convert) - - bottom_box.pack_start(widgetutil.align(self.convert_button, - xalign=0.5, yalign=0.5, - top_pad=50, bottom_pad=50)) - bottom.set_child(widgetutil.pad(bottom_box, left=20, right=20)) - vbox.pack_start(bottom) - self.window.set_content_widget(vbox) - - idle_add(self.conversion_manager.check_notifications, 1) - - self.window.connect('file-drag-motion', self.drag_motion) - self.window.connect('file-drag-received', self.drag_data_received) - self.window.connect('file-drag-leave', self.drag_finished) - self.window.accept_file_drag(True) - - self.window.center() - self.window.show() - self.update_table_size() - - def sort_converter_menu(self, menu_type, options): - """Sort a list of converter options for the menus - - :param menu_type: type of the menu - :param options: list of (name, menu) tuples, where menu is either a - ConverterInfo or list of ConverterInfos. - """ - if menu_type == 'format': - order = ['Audio', 'Video', 'Ingest Formats', 'Same Format'] - options.sort(key=lambda (name, menu): order.index(name)) - else: - options.sort() - - def drag_finished(self, widget): - self.drop_target.set_in_drag(False) - - def drag_motion(self, widget): - self.drop_target.set_in_drag(True) - - def drag_data_received(self, widget, values): - for uri in values: - parsed = urlparse.urlparse(uri) - if parsed.scheme == 'file': - pathname = urllib.url2pathname(parsed.path) - self.file_activated(widget, pathname) - - def on_window_shown(self, window): - # only emit window-shown once, even if our window gets shown, hidden, - # and shown again - if not self.sent_window_shown: - self.emit("window-shown") - self.sent_window_shown = True - - def destroy(self, widget): - for conversion in self.conversion_manager.in_progress.copy(): - conversion.stop() - mainloop_stop() - - def run(self): - mainloop_start() - - def choose_file(self): - dialog = widgetset.FileOpenDialog('Choose Files...') - dialog.set_select_multiple(True) - if dialog.run() == 0: # success - for filename in dialog.get_filenames(): - self.file_activated(None, filename) - dialog.destroy() - - def about(self): - dialog = widgetset.AboutDialog() - dialog.set_transient_for(self.window) - try: - dialog.run() - finally: - dialog.destroy() - - def quit(self): - self.window.close() - - def _generate_suboptions_menu(self, widget, options): - submenu = [] - for option, id_ in options: - callback = lambda x, i: self.on_select_converter(widget, - options[i][1]) - value = (option, callback) - submenu.append(value) - return submenu - - def show_options_menu(self, widget, options): - optionlist = [] - identifiers = dict() - for option, submenu in options: - if isinstance(submenu, list): - callback = self._generate_suboptions_menu(widget, submenu) - else: - callback = lambda x, i: self.on_select_converter(widget, - options[i][1]) - value = (option, callback) - optionlist.append(value) - menu = widgetset.ContextMenu(optionlist) - menu.popup() - - def update_convert_button(self): - can_cancel = False - can_start = False - has_conversions = any(self.model.conversions()) - all_done = self.model.all_conversions_done() - for c in self.model.conversions(): - if c.status == 'converting': - can_cancel = True - break - elif c.status == 'initialized': - can_start = True - # if there are no conversions ... these can't be set - if not has_conversions: - for m in self.menus: - m.set_selected(False) - self.settings_button.set_selected(False) - self.convert_label.set_color(TEXT_DISABLED) - # Set the colors - all are enabled if all conversions complete, or - # if we have conversions conversions but the converter has not yet - # been set. - # the converter has not been set. - if ((self.current_converter is EMPTY_CONVERTER and has_conversions) or - all_done): - for m in self.menus: - m.set_selected(True) - self.settings_button.set_selected(True) - if self.current_converter is EMPTY_CONVERTER: - self.convert_label.set_text('Convert to') - elif can_cancel: - target = self.current_converter.name - self.convert_label.set_text('Converting to %s' % target) - elif can_start: - target = self.current_converter.name - self.convert_label.set_text('Will convert to %s' % target) - self.convert_label.set_color(TEXT_ACTIVE) - if all_done: - self.convert_button.set_clear() - elif (self.current_converter is EMPTY_CONVERTER or not - (can_cancel or can_start)): - self.convert_button.set_off() - elif (self.current_converter is not EMPTY_CONVERTER and - self.options.options['custom-size'] and - (not self.options.options['width'] or - not self.options.options['height'])): - self.convert_button.set_off() - else: - self.convert_button.set_on() - if can_cancel: - self.convert_button.set_stop() - self.button_bar.disable() - else: - if has_conversions: - self.button_bar.enable() - else: - self.button_bar.disable() - - def file_activated(self, widget, filename): - filename = os.path.realpath(filename) - for c in self.model.conversions(): - if c.video.filename == filename: - logger.info('ignoring duplicate: %r', filename) - return - # XXX disabled - don't want to allow individualized file outputs - # since the workflow isn't entirely clear for now. - #if self.options.options['destination'] is None: - # try: - # tempfile.TemporaryFile(dir=os.path.dirname(filename)) - # except EnvironmentError: - # # can't write to the destination directory; ask for a new one - # self.options.on_destination_clicked(None) - try: - vf = VideoFile(filename) - except ValueError: - logging.info('invalid file %r, cannot parse', filename, - exc_info=True) - return - c = self.conversion_manager.get_conversion( - vf, - self.current_converter, - output_dir=self.options.options['destination']) - c.listen(self.update_conversion) - if self.conversion_manager.running: - # start running automatically if a conversion is already in - # progress - self.conversion_manager.run_conversion(c) - self.update_conversion(c) - self.update_table_size() - - def on_select_converter(self, widget, identifier): - self.current_converter = self.converter_manager.get_by_id(identifier) - self.options.reset() - - self.converter_changed(widget) - - def converter_changed(self, widget): - if hasattr(self, '_doing_conversion_change'): - return - self._doing_conversion_change = True - - # If all conversions are done, then change the status of them back - # to 'initialized'. - # - # XXX TODO: what happens if the state is 'failed'? Should we reset? - all_done = self.model.all_conversions_done() - if all_done: - for c in self.model.conversions(): - c.status = 'initialized' - - if self.current_converter is not EMPTY_CONVERTER: - self.convert_label.set_text( - 'Will convert to %s' % self.current_converter.name) - else: - self.convert_label.set_text('Convert to') - - if not self.current_converter.audio_only: - self.options.enable_custom_size() - self.options.update_setting('width', - self.current_converter.width) - self.options.update_setting('height', - self.current_converter.height) - else: - self.options.disable_custom_size() - - for c in self.model.conversions(): - if c.status == 'initialized': - c.set_converter(self.current_converter) - self.model.update_conversion(c) - - # We likely either reset the status or we've changed the conversion - # output so let's just reload the table model. - self.table.model_changed() - - self.update_convert_button() - - widget.set_selected(True) - for menu in self.menus: - if menu is not widget: - menu.set_selected(False) - - del self._doing_conversion_change - - def convert(self, widget): - self.convert_button.disable() - if not self.conversion_manager.running: - if self.current_converter is not EMPTY_CONVERTER: - valid_resolution = True - if (self.options.options['custom-size'] and - not (self.options.options['width'] and - self.options.options['height'])): - valid_resolution = False - if valid_resolution: - for conversion in self.model.conversions(): - if conversion.status == 'initialized': - self.conversion_manager.run_conversion(conversion) - self.button_bar.disable() - # all done: no conversion job should be running at this point - all_done = self.model.all_conversions_done() - if all_done: - # take stuff off one by one from the list until we have none! - # might not be very efficient. - iter_ = self.model.first_iter() - while iter_ is not None: - conversion = self.model[iter_][-1] - if conversion.status in ('finished', - 'failed', - 'canceled', - 'initialized'): - try: - self.conversion_manager.remove(conversion) - except ValueError: - pass - iter_ = self.model.remove(iter_) - self.update_table_size() - else: - for conversion in self.model.conversions(): - conversion.stop() - self.update_conversion(conversion) - self.conversion_manager.running = False - self.update_convert_button() - self.convert_button.enable() - - def update_conversion(self, conversion): - self.model.update_conversion(conversion) - self.update_table_size() - - def update_table_size(self): - conversions = len(self.model) - total_height = 380 - if not conversions: - self.scroller.set_size_request(-1, 0) - self.drop_target.set_small(False) - self.drop_target.set_size_request(-1, total_height) - else: - height = min(TABLE_HEIGHT * conversions, 320) - self.scroller.set_size_request(-1, height) - self.drop_target.set_small(True) - self.drop_target.set_size_request(-1, total_height - height) - self.update_convert_button() - self.table.model_changed() - - def hotspot_clicked(self, widget, name, iter_): - conversion = self.model[iter_][-1] - if name == 'show-file': - reveal_file(conversion.output) - elif name == 'clear': - self.model.remove(iter_) - self.update_table_size() - elif name == 'show-log': - lines = ''.join(conversion.lines) - d = TextDialog('Log', '', self.window) - d.set_text(lines) - try: - d.run() - finally: - d.destroy() - elif name == 'cancel': - if conversion.status == 'initialized': - self.model.remove(iter_) - try: - self.conversion_manager.remove(conversion) - except ValueError: - pass - self.update_table_size() - else: - conversion.stop() - self.update_conversion(conversion) - - def on_settings_toggle(self, widget): - if not self.options.child: - # hidden, going to show - self.convert_button.hide() - self.options.toggle() - if not self.options.child: - # was shown, not hidden - self.convert_button.show() - - def on_setting_changed(self, widget, setting, value): - if setting == 'destination': - for c in self.model.conversions(): - if c.status == 'initialized': - if value is None: - c.output_dir = os.path.dirname(c.video.filename) - else: - c.output_dir = value - # update final path - c.set_converter(self.current_converter) - return - elif setting == 'dont-upsize': - setattr(self.current_converter, 'dont_upsize', value) - return - - if (self.current_converter.identifier != 'custom' and - setting != 'create-thumbnails'): - if hasattr(self.current_converter, 'simple'): - self.current_converter = self.current_converter.simple( - self.current_converter.name) - else: - if self.current_converter is EMPTY_CONVERTER: - self.current_converter = copy.copy(self.converter_manager.get_by_id('sameformat')) - else: - self.current_converter = copy.copy(self.current_converter) - # If the current converter name is resize only, then we don't - # want to call it a custom conversion. - if self.current_converter.identifier != 'sameformat': - self.current_converter.name = 'Custom' - self.current_converter.width = self.options.options['width'] - self.current_converter.height = self.options.options['height'] - self.converter_changed(self.menus[-1]) # formats menu - if setting in ('width', 'height'): - setattr(self.current_converter, setting, value) - elif setting == 'custom-size': - if not value: - self.current_converter.old_size = ( - self.current_converter.width, - self.current_converter.height) - self.current_converter.width = None - self.current_converter.height = None - elif hasattr(self.current_converter, 'old_size'): - old_size = self.current_converter.old_size - (self.current_converter.width, - self.current_converter.height) = old_size - elif setting == 'create-thumbnails': - self.conversion_manager.create_thumbnails = bool(value) - -if __name__ == "__main__": - sys.dont_write_bytecode = True - app.widgetapp = Application() - initialize(app.widgetapp) diff --git a/mvc/utils.py b/mvc/utils.py deleted file mode 100644 index e0a64f3..0000000 --- a/mvc/utils.py +++ /dev/null @@ -1,230 +0,0 @@ -import ctypes -import itertools -import logging -import os -import sys - -def hms_to_seconds(hours, minutes, seconds): - return (hours * 3600 + - minutes * 60 + - seconds) - - -def round_even(num): - """This takes a number, converts it to an integer, then makes - sure it's even. - - Additional rules: this helper always rounds down to avoid stray black - pixels (see bz18122). - - This function makes sure that the value returned is always >= 0. - """ - num = int(num) - val = num - (num % 2) - return val if val > 0 else 0 - - -def rescale_video((source_width, source_height), - (target_width, target_height), - dont_upsize=True): - """ - Rescale a video given a (width, height) target. This returns the largest - (width, height) which maintains the original aspect ratio while fitting - within the target size. - - If dont_upsize is set, then don't resize it such that the rescaled size - will be larger than the original size. - """ - if source_width is None or source_height is None: - return (round_even(target_width), round_even(target_height)) - - if target_width is None or target_height is None: - return (round_even(source_width), round_even(source_height)) - - if (dont_upsize and - (source_width <= target_width or source_height <= target_height)): - return (round_even(source_width), round_even(source_height)) - - width_ratio = float(source_width) / float(target_width) - height_ratio = float(source_height) / float(target_height) - ratio = max(width_ratio, height_ratio) - return round_even(source_width / ratio), round_even(source_height / ratio) - -def line_reader(handle): - """Builds a line reading generator for the given handle. This - generator breaks on empty strings, \\r and \\n. - - This a little weird, but it makes it really easy to test error - checking and progress monitoring. - """ - def _readlines(): - chars = [] - c = handle.read(1) - while True: - if c in ["", "\r", "\n"]: - if chars: - yield "".join(chars) - if not c: - break - chars = [] - else: - chars.append(c) - c = handle.read(1) - return _readlines() - - -class Matrix(object): - """2 Dimensional matrix. - - Matrix objects are accessed like a list, except tuples are used as - indices, for example: - - >>> m = Matrix(5, 5) - >>> m[3, 4] = 'foo' - >>> m - None, None, None, None, None - None, None, None, None, None - None, None, None, None, None - None, None, None, None, None - None, None, None, 'foo', None - """ - - def __init__(self, columns, rows, initial_value=None): - self.columns = columns - self.rows = rows - self.data = [ initial_value ] * (columns * rows) - - def __getitem__(self, key): - return self.data[(key[0] * self.rows) + key[1]] - - def __setitem__(self, key, value): - self.data[(key[0] * self.rows) + key[1]] = value - - def __iter__(self): - return iter(self.data) - - def __repr__(self): - return "\n".join([", ".join([repr(r) - for r in list(self.row(i))]) - for i in xrange(self.rows)]) - - def remove(self, value): - """This sets the value to None--it does NOT remove the cell - from the Matrix because that doesn't make any sense. - """ - i = self.data.index(value) - self.data[i] = None - - def row(self, row): - """Iterator that yields all the objects in a row.""" - for i in xrange(self.columns): - yield self[i, row] - - def column(self, column): - """Iterator that yields all the objects in a column.""" - for i in xrange(self.rows): - yield self[column, i] - - -class Cache(object): - def __init__(self, size): - self.size = size - self.dict = {} - self.counter = itertools.count() - self.access_times = {} - self.invalidators = {} - - def get(self, key, invalidator=None): - if key in self.dict: - existing_invalidator = self.invalidators[key] - if (existing_invalidator is None or - not existing_invalidator(key)): - self.access_times[key] = self.counter.next() - return self.dict[key] - - value = self.create_new_value(key, invalidator=invalidator) - self.set(key, value, invalidator=invalidator) - return value - - def set(self, key, value, invalidator=None): - if len(self.dict) == self.size: - self.shrink_size() - self.access_times[key] = self.counter.next() - self.dict[key] = value - self.invalidators[key] = invalidator - - def remove(self, key): - if key in self.dict: - del self.dict[key] - del self.access_times[key] - if key in self.invalidators: - del self.invalidators[key] - - def keys(self): - return self.dict.iterkeys() - - def shrink_size(self): - # shrink by LRU - to_sort = self.access_times.items() - to_sort.sort(key=lambda m: m[1]) - new_dict = {} - new_access_times = {} - new_invalidators = {} - latest_times = to_sort[len(self.dict) // 2:] - for (key, time) in latest_times: - new_dict[key] = self.dict[key] - new_invalidators[key] = self.invalidators[key] - new_access_times[key] = time - self.dict = new_dict - self.access_times = new_access_times - - def create_new_value(self, val, invalidator=None): - raise NotImplementedError() - - -def size_string(nbytes): - # when switching from the enclosure reported size to the - # downloader reported size, it takes a while to get the new size - # and the downloader returns -1. the user sees the size go to -1B - # which is weird.... better to return an empty string. - if nbytes == -1 or nbytes == 0: - return "" - - # FIXME this is a repeat of util.format_size_for_user ... should - # probably ditch one of them. - if nbytes >= (1 << 30): - value = "%.1f" % (nbytes / float(1 << 30)) - return "%(size)s GB" % {"size": value} - elif nbytes >= (1 << 20): - value = "%.1f" % (nbytes / float(1 << 20)) - return "%(size)s MB" % {"size": value} - elif nbytes >= (1 << 10): - value = "%.1f" % (nbytes / float(1 << 10)) - return "%(size)s KB" % {"size": value} - else: - return "%(size)s B" % {"size": nbytes} - -def convert_path_for_subprocess(path): - """Convert a path to a form suitable for passing to a subprocess. - - This method converts unicode paths to bytestrings according to the system - fileencoding. On windows, it converts the path to a short filename for - maximum compatibility - - This method should only be called on a path that exists on the filesystem. - """ - if not os.path.exists(path): - raise ValueError("path %r doesn't exist" % path) - if not isinstance(path, unicode): - # path already is a bytestring, just return it - return path - if sys.platform != 'win32': - return path.encode(sys.getfilesystemencoding()) - else: - buf_size = 1024 - short_path_buf = ctypes.create_unicode_buffer(buf_size) - ctypes.windll.kernel32.GetShortPathNameW(path, - short_path_buf, buf_size) - logging.info("convert_path_for_subprocess: got short path %r", - short_path_buf.value) - return short_path_buf.value.encode('ascii') diff --git a/mvc/video.py b/mvc/video.py deleted file mode 100644 index 0b89b63..0000000 --- a/mvc/video.py +++ /dev/null @@ -1,287 +0,0 @@ -import logging -import os -import re -import tempfile -import threading - -from mvc import execute -from mvc.widgets import idle_add -from mvc.settings import get_ffmpeg_executable_path -from mvc.utils import hms_to_seconds, convert_path_for_subprocess - -logger = logging.getLogger(__name__) - -class VideoFile(object): - def __init__(self, filename): - self.filename = filename - self.container = None - self.video_codec = None - self.audio_codec = None - self.width = None - self.height = None - self.duration = None - self.thumbnails = {} - self.parse() - - def parse(self): - self.__dict__.update( - get_media_info(self.filename)) - - @property - def audio_only(self): - return self.video_codec is None - - def get_thumbnail(self, completion, width=None, height=None, type_='.png'): - if self.audio_only: - # don't bother with thumbnails for audio files - return None - if width is None: - width = -1 - if height is None: - height = -1 - - if self.duration is None: - skip = 0 - else: - skip = min(int(self.duration / 3), 120) - - key = (width, height, type_) - - def complete(name): - self.thumbnails[key] = name - completion() - - if key not in self.thumbnails: - temp_path = tempfile.mktemp(suffix=type_) - get_thumbnail(self.filename, width, height, temp_path, complete, - skip=skip) - return None - - return self.thumbnails.get(key) - -class Node(object): - def __init__(self, line="", children=None): - self.line = line - if not children: - self.children = [] - else: - self.children = children - - if ": " in line: - self.key, self.value = line.split(": ", 1) - else: - self.key = "" - self.value = "" - - def add_node(self, node): - self.children.append(node) - - def pformat(self, indent=0): - s = (" " * indent) + ("Node: %s" % self.line) + "\n" - for mem in self.children: - s += mem.pformat(indent + 2) - return s - - def get_by_key(self, key): - if self.line.startswith(key): - return self - for mem in self.children: - ret = mem.get_by_key(key) - if ret: - return ret - return None - - def __repr__(self): - return "<Node %s: %s>" % (self.key, self.value) - - -def get_indent(line): - length = len(line) - line = line.lstrip() - return (length - len(line), line) - - -def parse_ffmpeg_output(output): - """Takes a list of strings and parses it into a loose AST-ish - thing. - - ffmpeg output uses indentation levels to indicate a hierarchy of - data. - - If there's a : in the line, then it's probably a key/value pair. - - :param output: the content to parse as a list of strings. - - :returns: a top level node of the ffmpeg output AST - """ - ast = Node() - node_stack = [ast] - indent_level = 0 - - for mem in output: - # skip blank lines - if len(mem.strip()) == 0: - continue - - indent, line = get_indent(mem) - node = Node(line) - - if indent == indent_level: - node_stack[-1].add_node(node) - elif indent > indent_level: - node_stack.append(node_stack[-1].children[-1]) - indent_level = indent - node_stack[-1].add_node(node) - else: - for dedent in range(indent, indent_level, 2): - # make sure we never pop everything off the stack. - # the root should always be on the stack. - if len(node_stack) <= 1: - break - node_stack.pop() - indent_level = indent - node_stack[-1].add_node(node) - - return ast - - -# there's always a space before the size and either a space or a comma -# afterwards. -SIZE_RE = re.compile(" (\\d+)x(\\d+)[ ,]") - - -def extract_info(ast): - info = {} - # logging.info("get_media_info: %s", ast.pformat()) - - input0 = ast.get_by_key("Input #0") - if not input0: - raise ValueError("no input #0") - - foo, info['container'], bar = input0.line.split(', ', 2) - if ',' in info['container']: - info['container'] = info['container'].split(',') - - metadata = input0.get_by_key("Metadata") - if metadata: - for key in ('title', 'artist', 'album', 'track', 'genre'): - node = metadata.get_by_key(key) - if node: - info[key] = node.line.split(':', 1)[1].strip() - major_brand_node = metadata.get_by_key("major_brand") - extra_container_types = [] - if major_brand_node: - major_brand = major_brand_node.line.split(':')[1].strip() - extra_container_types = [major_brand] - else: - major_brand = None - - compatible_brands_node = metadata.get_by_key("compatible_brands") - if compatible_brands_node: - line = compatible_brands_node.line.split(':')[1].strip() - extra_container_types.extend(line[i:i+4] for i in range(0, len(line), 4) - if line[i:i+4] != major_brand) - - if extra_container_types: - if not isinstance(info['container'], list): - info['container'] = [info['container']] - info['container'].extend(extra_container_types) - - duration = input0.get_by_key("Duration:") - if duration: - _, rest = duration.line.split(':', 1) - duration_string, _ = rest.split(', ', 1) - logging.info("duration: %r", duration_string) - try: - hours, minutes, seconds = [ - float(i) for i in duration_string.split(':')] - except ValueError: - if duration_string.strip() != "N/A": - logging.warn("Error parsing duration string: %r", - duration_string) - else: - info['duration'] = hms_to_seconds(hours, minutes, seconds) - for stream_node in duration.children: - stream = stream_node.line - if "Video:" in stream: - stream_number, video, data = stream.split(': ', 2) - video_codec = data.split(', ')[0] - if ' ' in video_codec: - video_codec, drmp = video_codec.split(' ', 1) - if 'drm' in drmp: - info.setdefault('has_drm', []).append('video') - info['video_codec'] = video_codec - match = SIZE_RE.search(data) - if match: - info["width"] = int(match.group(1)) - info["height"] = int(match.group(2)) - elif 'Audio:' in stream: - stream_number, video, data = stream.split(': ', 2) - audio_codec = data.split(', ')[0] - if ' ' in audio_codec: - audio_codec, drmp = audio_codec.split(' ', 1) - if 'drm' in drmp: - info.setdefault('has_drm', []).append('audio') - info['audio_codec'] = audio_codec - return info - -def get_ffmpeg_output(filepath): - - commandline = [get_ffmpeg_executable_path(), - "-i", convert_path_for_subprocess(filepath)] - logging.info("get_ffmpeg_output(): running %s", commandline) - try: - output = execute.check_output(commandline) - except execute.CalledProcessError, e: - if e.returncode != 1: - logger.exception("error calling %r\noutput:%s", commandline, - e.output) - # ffmpeg -i generally returns 1, so we ignore the exception and - # just get the output. - output = e.output - - return output - -def get_media_info(filepath): - """Takes a file path and returns a dict of information about - this media file that it extracted from ffmpeg -i. - - :param filepath: absolute path to the media file in question - - :returns: dict of media info possibly containing: height, width, - container, audio_codec, video_codec - """ - logger.info('get_media_info: %r', filepath) - output = get_ffmpeg_output(filepath) - ast = parse_ffmpeg_output(output.splitlines()) - info = extract_info(ast) - logger.info('get_media_info: %r', info) - return info - -def get_thumbnail(filename, width, height, output, completion, skip=0): - name = 'Thumbnail - %r @ %sx%s' % (filename, width, height) - def run(): - rv = get_thumbnail_synchronous(filename, width, height, output, skip) - idle_add(lambda: completion(rv)) - t = threading.Thread(target=run, name=name) - t.start() - -def get_thumbnail_synchronous(filename, width, height, output, skip=0): - executable = get_ffmpeg_executable_path() - filter_ = 'scale=%i:%i' % (width, height) - # bz19571: temporary disable: libav ffmpeg does not support this filter - #if 'ffmpeg' in executable: - # # supports the thumbnail filter, we hope - # filter_ = 'thumbnail,' + filter_ - commandline = [executable, - '-ss', str(skip), - '-i', convert_path_for_subprocess(filename), - '-vf', filter_, '-vframes', '1', output] - try: - execute.check_output(commandline) - except execute.CalledProcessError, e: - logger.exception('error calling %r\ncode:%s\noutput:%s', - commandline, e.returncode, e.output) - return None - else: - return output diff --git a/mvc/widgets/__init__.py b/mvc/widgets/__init__.py deleted file mode 100644 index 23a6edc..0000000 --- a/mvc/widgets/__init__.py +++ /dev/null @@ -1,30 +0,0 @@ -import logging -import os -import sys - -if sys.platform == 'darwin': - import osx as plat - from .osx import widgetset -else: - import gtk as plat - from .gtk import widgetset - -attach_menubar = plat.attach_menubar -mainloop_start = plat.mainloop_start -mainloop_stop = plat.mainloop_stop -idle_add = plat.idle_add -idle_remove = plat.idle_remove -reveal_file = plat.reveal_file -get_conversion_directory = plat.get_conversion_directory - -def get_conversion_directory(): - return os.path.join(plat.get_conversion_directory(), 'Libre Video Converter') - """directorio donde se guardan los videos convertidos""" - -def initialize(app): - try: - os.makedirs(get_conversion_directory()) - except EnvironmentError, e: - logging.info('os.makedirs: %s', str(e)) - if app: - plat.initialize(app) diff --git a/mvc/widgets/app.py b/mvc/widgets/app.py deleted file mode 100644 index 531b745..0000000 --- a/mvc/widgets/app.py +++ /dev/null @@ -1,4 +0,0 @@ -# app.py - -widgetapp = None - diff --git a/mvc/widgets/cellpack.py b/mvc/widgets/cellpack.py deleted file mode 100644 index 1347f56..0000000 --- a/mvc/widgets/cellpack.py +++ /dev/null @@ -1,843 +0,0 @@ -"""``miro.frontends.widgets.cellpack`` -- Code to layout -CustomTableCells. - -We use the hbox/vbox model to lay things out with a couple changes. -The main difference here is that layouts are one-shot. We don't keep -state around inside the cell renderers, so we just set up the objects -at the start, then use them to calculate info. -""" - -class Margin(object): - """Helper object used to calculate margins. - """ - def __init__(self , margin): - if margin is None: - margin = (0, 0, 0, 0) - self.margin_left = margin[3] - self.margin_top = margin[0] - self.margin_width = margin[1] + margin[3] - self.margin_height = margin[0] + margin[2] - - def inner_rect(self, x, y, width, height): - """Returns the x, y, width, height of the inner - box. - """ - return (x + self.margin_left, - y + self.margin_top, - width - self.margin_width, - height - self.margin_height) - - def outer_size(self, inner_size): - """Returns the width, height of the outer box. - """ - return (inner_size[0] + self.margin_width, - inner_size[1] + self.margin_height) - - def point_in_margin(self, x, y, width, height): - """Returns whether a given point is inside of the - margins. - """ - return ((0 <= x - self.margin_left < width - self.margin_width) and - (0 <= y - self.margin_top < height - self.margin_height)) - -class Packing(object): - """Helper object used to layout Boxes. - """ - def __init__(self, child, expand): - self.child = child - self.expand = expand - - def calc_size(self, translate_func): - return translate_func(*self.child.get_size()) - - def draw(self, context, x, y, width, height): - self.child.draw(context, x, y, width, height) - -class WhitespacePacking(object): - """Helper object used to layout Boxes. - """ - def __init__(self, size, expand): - self.size = size - self.expand = expand - - def calc_size(self, translate_func): - return self.size, 0 - - def draw(self, context, x, y, width, height): - pass - -class Packer(object): - """Base class packing objects. Packer objects work similarly to widgets, - but they only used in custom cell renderers so there's a couple - differences. The main difference is that cell renderers don't keep state - around. Therefore Packers just get set up, used, then discarded. - Also Packers can't receive events directly, so they have a different - system to figure out where mouse clicks happened (the Hotspot class). - """ - - def render_layout(self, context): - """position the child elements then call draw() on them.""" - self._layout(context, 0, 0, context.width, context.height) - - def draw(self, context, x, y, width, height): - """Included so that Packer objects have a draw() method that matches - ImageSurfaces, TextBoxes, etc. - """ - self._layout(context, x, y, width, height) - - def _find_child_at(self, x, y, width, height): - raise NotImplementedError() - - def get_size(self): - """Get the minimum size required to hold the Packer. """ - try: - return self._size - except AttributeError: - self._size = self._calc_size() - return self._size - - def get_current_size(self): - """Get the minimum size required to hold the Packer at this point - - Call this method if you are going to change the packer after the call, - for example if you have more children to pack into a box. get_size() - saves caches it's result which is can mess things up. - """ - return self._calc_size() - - def find_hotspot(self, x, y, width, height): - """Find the hotspot at (x, y). width and height are the size of the - cell this Packer is rendering. - - If a hotspot is found, return the tuple (name, x, y, width, height) - where name is the name of the hotspot, x, y is the position relative - to the top-left of the hotspot area and width, height are the - dimensions of the hotspot. - - If no Hotspot is found return None. - """ - child_pos = self._find_child_at(x, y, width, height) - if child_pos: - child, child_x, child_y, child_width, child_height = child_pos - try: - return child.find_hotspot(x - child_x, y - child_y, - child_width, child_height) - except AttributeError: - pass # child is a TextBox, Button or something like that - return None - - def _layout(self, context, x, y, width, height): - """Layout our children and call ``draw()`` on them. - """ - raise NotImplementedError() - - def _calc_size(self): - """Calculate the size needed to hold the box. The return value gets - cached and return in ``get_size()``. - """ - raise NotImplementedError() - -class Box(Packer): - """Box is the base class for VBox and HBox. Box objects lay out children - linearly either left to right or top to bottom. - """ - - def __init__(self, spacing=0): - """Create a new Box. spacing is the amount of space to place - in-between children. - """ - self.spacing = spacing - self.children = [] - self.children_end = [] - self.expand_count = 0 - - def pack(self, child, expand=False): - """Add a new child to the box. The child will be placed after all the - children packed before with pack_start. - - :param child: child to pack. It can be anything with a - ``get_size()`` method, including TextBoxes, - ImageSurfarces, Buttons, Boxes and Backgrounds. - :param expand: If True, then the child will enlarge if space - available is more than the space required. - """ - if not (hasattr(child, 'draw') and hasattr(child, 'get_size')): - raise TypeError("%s can't be drawn" % child) - self.children.append(Packing(child, expand)) - if expand: - self.expand_count += 1 - - def pack_end(self, child, expand=False): - """Add a new child to the end box. The child will be placed before - all the children packed before with pack_end. - - :param child: child to pack. It can be anything with a - ``get_size()`` method, including TextBoxes, - ImageSurfarces, Buttons, Boxes and Backgrounds. - :param expand: If True, then the child will enlarge if space - available is more than the space required. - """ - if not (hasattr(child, 'draw') and hasattr(child, 'get_size')): - raise TypeError("%s can't be drawn" % child) - self.children_end.append(Packing(child, expand)) - if expand: - self.expand_count += 1 - - def pack_space(self, size, expand=False): - """Pack whitespace into the box. - """ - self.children.append(WhitespacePacking(size, expand)) - if expand: - self.expand_count += 1 - - def pack_space_end(self, size, expand=False): - """Pack whitespace into the end of box. - """ - self.children_end.append(WhitespacePacking(size, expand)) - if expand: - self.expand_count += 1 - - def _calc_size(self): - length = 0 - breadth = 0 - for packing in self.children + self.children_end: - child_length, child_breadth = packing.calc_size(self._translate) - length += child_length - breadth = max(breadth, child_breadth) - total_children = len(self.children) + len(self.children_end) - length += self.spacing * (total_children - 1) - return self._translate(length, breadth) - - def _extra_space_iter(self, total_extra_space): - """Generate the amount of extra space for children with expand set.""" - if total_extra_space <= 0: - while True: - yield 0 - average_extra_space, leftover = \ - divmod(total_extra_space, self.expand_count) - while leftover > 1: - # expand_count doesn't divide equally into total_extra_space, - # yield average_extra_space+1 for each extra pixel - yield average_extra_space + 1 - leftover -= 1 - # if there's a fraction of a pixel leftover, add that in - yield average_extra_space + leftover - while True: - # no more leftover space - yield average_extra_space - - def _position_children(self, total_length): - my_length, my_breadth = self._translate(*self.get_size()) - extra_space_iter = self._extra_space_iter(total_length - my_length) - - pos = 0 - for packing in self.children: - child_length, child_breadth = packing.calc_size(self._translate) - if packing.expand: - child_length += extra_space_iter.next() - yield packing, pos, child_length - pos += child_length + self.spacing - - pos = total_length - for packing in self.children_end: - child_length, child_breadth = packing.calc_size(self._translate) - if packing.expand: - child_length += extra_space_iter.next() - pos -= child_length - yield packing, pos, child_length - pos -= self.spacing - - def _layout(self, context, x, y, width, height): - total_length, total_breadth = self._translate(width, height) - pos, offset = self._translate(x, y) - position_iter = self._position_children(total_length) - for packing, child_pos, child_length in position_iter: - x, y = self._translate(pos + child_pos, offset) - width, height = self._translate(child_length, total_breadth) - packing.draw(context, x, y, width, height) - - def _find_child_at(self, x, y, width, height): - total_length, total_breadth = self._translate(width, height) - pos, offset = self._translate(x, y) - position_iter = self._position_children(total_length) - for packing, child_pos, child_length in position_iter: - if child_pos <= pos < child_pos + child_length: - x, y = self._translate(child_pos, 0) - width, height = self._translate(child_length, total_breadth) - if isinstance(packing, WhitespacePacking): - return None - return packing.child, x, y, width, height - elif child_pos > pos: - break - return None - - def _translate(self, x, y): - """Translate (x, y) coordinates into (length, breadth) and - vice-versa. - """ - raise NotImplementedError() - -class HBox(Box): - def _translate(self, x, y): - return x, y - -class VBox(Box): - def _translate(self, x, y): - return y, x - -class Table(Packer): - def __init__(self, row_length=1, col_length=1, - row_spacing=0, col_spacing=0): - """Create a new Table. - - :param row_length: how many rows long this should be - :param col_length: how many rows wide this should be - :param row_spacing: amount of spacing (in pixels) between rows - :param col_spacing: amount of spacing (in pixels) between columns - """ - assert min(row_length, col_length) > 0 - assert isinstance(row_length, int) and isinstance(col_length, int) - self.row_length = row_length - self.col_length = col_length - self.row_spacing = row_spacing - self.col_spacing = col_spacing - self.table_multiarray = self._generate_table_multiarray() - - def _generate_table_multiarray(self): - table_multiarray = [] - table_multiarray = [ - [None for col in range(self.col_length)] - for row in range(self.row_length)] - return table_multiarray - - def pack(self, child, row, column, expand=False): - # TODO: flesh out "expand" ability, maybe? - # - # possibly throw a special exception if outside the range. - # For now, just allowing an IndexError to be thrown. - self.table_multiarray[row][column] = Packing(child, expand) - - def _get_grid_sizes(self): - """Get the width and eights for both rows and columns - """ - row_sizes = {} - col_sizes = {} - for row_count, row in enumerate(self.table_multiarray): - row_sizes.setdefault(row_count, 0) - for col_count, col_packing in enumerate(row): - col_sizes.setdefault(col_count, 0) - if col_packing: - x, y = col_packing.calc_size(self._translate) - if y > row_sizes[row_count]: - row_sizes[row_count] = y - if x > col_sizes[col_count]: - col_sizes[col_count] = x - return col_sizes, row_sizes - - def _find_child_at(self, x, y, width, height): - col_sizes, row_sizes = self._get_grid_sizes() - row_distance = 0 - for row_count, row in enumerate(self.table_multiarray): - col_distance = 0 - for col_count, packing in enumerate(row): - child_width, child_height = packing.calc_size(self._translate) - if packing.child: - if (col_distance <= x < col_distance + child_width - and row_distance <= y < row_distance + child_height): - return (packing.child, - col_distance, row_distance, - child_width, child_height) - col_distance += col_sizes[col_count] + self.col_spacing - row_distance += row_sizes[row_count] + self.row_spacing - - def _calc_size(self): - col_sizes, row_sizes = self._get_grid_sizes() - x = sum(col_sizes.values()) + ( - (self.col_length - 1) * self.col_spacing) - y = sum(row_sizes.values()) + ( - (self.row_length - 1) * self.row_spacing) - return x, y - - def _layout(self, context, x, y, width, height): - col_sizes, row_sizes = self._get_grid_sizes() - - row_distance = 0 - for row_count, row in enumerate(self.table_multiarray): - col_distance = 0 - for col_count, packing in enumerate(row): - if packing: - child_width, child_height = packing.calc_size( - self._translate) - packing.child.draw(context, - x + col_distance, y + row_distance, - child_width, child_height) - col_distance += col_sizes[col_count] + self.col_spacing - row_distance += row_sizes[row_count] + self.row_spacing - - def _translate(self, x, y): - return x, y - - -class Alignment(Packer): - """Positions a child inside a larger space. - """ - def __init__(self, child, xscale=1.0, yscale=1.0, xalign=0.0, yalign=0.0, - min_width=0, min_height=0): - self.child = child - self.xscale = xscale - self.yscale = yscale - self.xalign = xalign - self.yalign = yalign - self.min_width = min_width - self.min_height = min_height - - def _calc_size(self): - width, height = self.child.get_size() - return max(self.min_width, width), max(self.min_height, height) - - def _calc_child_position(self, width, height): - req_width, req_height = self.child.get_size() - child_width = req_width + self.xscale * (width-req_width) - child_height = req_height + self.yscale * (height-req_height) - child_x = round(self.xalign * (width - child_width)) - child_y = round(self.yalign * (height - child_height)) - return child_x, child_y, child_width, child_height - - def _layout(self, context, x, y, width, height): - child_x, child_y, child_width, child_height = \ - self._calc_child_position(width, height) - self.child.draw(context, x + child_x, y + child_y, child_width, - child_height) - - def _find_child_at(self, x, y, width, height): - child_x, child_y, child_width, child_height = \ - self._calc_child_position(width, height) - if ((child_x <= x < child_x + child_width) and - (child_y <= y < child_y + child_height)): - return self.child, child_x, child_y, child_width, child_height - else: - return None # (x, y) is in the empty space around child - -class DrawingArea(Packer): - """Area that uses custom drawing code. - """ - def __init__(self, width, height, callback, *args): - self.width = width - self.height = height - self.callback_info = (callback, args) - - def _calc_size(self): - return self.width, self.height - - def _layout(self, context, x, y, width, height): - callback, args = self.callback_info - callback(context, x, y, width, height, *args) - - def _find_child_at(self, x, y, width, height): - return None - -class Background(Packer): - """Draws a background behind a child element. - """ - def __init__(self, child, min_width=0, min_height=0, margin=None): - self.child = child - self.min_width = min_width - self.min_height = min_height - self.margin = Margin(margin) - self.callback_info = None - - def set_callback(self, callback, *args): - self.callback_info = (callback, args) - - def _calc_size(self): - width, height = self.child.get_size() - width = max(self.min_width, width) - height = max(self.min_height, height) - return self.margin.outer_size((width, height)) - - def _layout(self, context, x, y, width, height): - if self.callback_info: - callback, args = self.callback_info - callback(context, x, y, width, height, *args) - self.child.draw(context, *self.margin.inner_rect(x, y, width, height)) - - def _find_child_at(self, x, y, width, height): - if not self.margin.point_in_margin(x, y, width, height): - return None - return (self.child,) + self.margin.inner_rect(0, 0, width, height) - -class Padding(Packer): - """Adds padding to the edges of a packer. - """ - def __init__(self, child, top=0, right=0, bottom=0, left=0): - self.child = child - self.margin = Margin((top, right, bottom, left)) - - def _calc_size(self): - return self.margin.outer_size(self.child.get_size()) - - def _layout(self, context, x, y, width, height): - self.child.draw(context, *self.margin.inner_rect(x, y, width, height)) - - def _find_child_at(self, x, y, width, height): - if not self.margin.point_in_margin(x, y, width, height): - return None - return (self.child,) + self.margin.inner_rect(0, 0, width, height) - -class TextBoxPacker(Packer): - """Base class for ClippedTextLine and ClippedTextBox. - """ - def _layout(self, context, x, y, width, height): - self.textbox.draw(context, x, y, width, height) - - def _find_child_at(self, x, y, width, height): - # We could return the TextBox here, but we know it doesn't have a - # find_hotspot() method - return None - -class ClippedTextBox(TextBoxPacker): - """A TextBox that gets clipped if it's larger than it's allocated - width. - """ - def __init__(self, textbox, min_width=0, min_height=0): - self.textbox = textbox - self.min_width = min_width - self.min_height = min_height - - def _calc_size(self): - height = max(self.min_height, self.textbox.font.line_height()) - return self.min_width, height - -class ClippedTextLine(TextBoxPacker): - """A single line of text that gets clipped if it's larger than the - space allocated to it. By default the clipping will happen at character - boundaries. - """ - def __init__(self, textbox, min_width=0): - self.textbox = textbox - self.textbox.set_wrap_style('char') - self.min_width = min_width - - def _calc_size(self): - return self.min_width, self.textbox.font.line_height() - -class TruncatedTextLine(ClippedTextLine): - def __init__(self, textbox, min_width=0): - ClippedTextLine.__init__(self, textbox, min_width) - self.textbox.set_wrap_style('truncated-char') - -class Hotspot(Packer): - """A Hotspot handles mouse click tracking. It's only purpose is - to store a name to return from ``find_hotspot()``. In terms of - layout, it simply renders it's child in it's allocated space. - """ - def __init__(self, name, child): - self.name = name - self.child = child - - def _calc_size(self): - return self.child.get_size() - - def _layout(self, context, x, y, width, height): - self.child.draw(context, x, y, width, height) - - def find_hotspot(self, x, y, width, height): - return self.name, x, y, width, height - -class Stack(Packer): - """Packer that stacks other packers on top of each other. - """ - def __init__(self): - self.children = [] - - def pack(self, packer): - self.children.append(packer) - - def pack_below(self, packer): - self.children.insert(0, packer) - - def _layout(self, context, x, y, width, height): - for packer in self.children: - packer._layout(context, x, y, width, height) - - def _calc_size(self): - """Calculate the size needed to hold the box. The return value gets - cached and return in get_size(). - """ - width = height = 0 - for packer in self.children: - child_width, child_height = packer.get_size() - width = max(width, child_width) - height = max(height, child_height) - return width, height - - def _find_child_at(self, x, y, width, height): - # Return the topmost packer - try: - top = self.children[-1] - except IndexError: - return None - else: - return top._find_child_at(x, y, width, height) - -def align_left(packer): - """Align a packer to the left side of it's allocated space.""" - return Alignment(packer, xalign=0.0, xscale=0.0) - -def align_right(packer): - """Align a packer to the right side of it's allocated space.""" - return Alignment(packer, xalign=1.0, xscale=0.0) - -def align_top(packer): - """Align a packer to the top side of it's allocated space.""" - return Alignment(packer, yalign=0.0, yscale=0.0) - -def align_bottom(packer): - """Align a packer to the bottom side of it's allocated space.""" - return Alignment(packer, yalign=1.0, yscale=0.0) - -def align_middle(packer): - """Align a packer to the middle of it's allocated space.""" - return Alignment(packer, yalign=0.5, yscale=0.0) - -def align_center(packer): - """Align a packer to the center of it's allocated space.""" - return Alignment(packer, xalign=0.5, xscale=0.0) - -def pad(packer, top=0, left=0, bottom=0, right=0): - """Add padding to a packer.""" - return Padding(packer, top, right, bottom, left) - -class LayoutRect(object): - """Lightweight object use to track rectangles inside a layout - - :attribute x: top coordinate, read-write - :attribute y: left coordinate, read-write - :attribute width: width of the rect, read-write - :attribute height: height of the rect, read-write - """ - - def __init__(self, x, y, width, height): - self.x = x - self.y = y - self.width = width - self.height = height - - def __str__(self): - return "LayoutRect(%s, %s, %s, %s)" % (self.x, self.y, self.width, - self.height) - - def __eq__(self, other): - my_values = (self.x, self.y, self.width, self.height) - try: - other_values = (other.x, other.y, other.width, other.height) - except AttributeError: - return NotImplemented - return my_values == other_values - - def subsection(self, left, right, top, bottom): - """Create a new LayoutRect from inside this one.""" - return LayoutRect(self.x + left, self.y + top, - self.width - left - right, self.height - top - bottom) - - def right_side(self, width): - """Create a new LayoutRect from the right side of this one.""" - return LayoutRect(self.right - width, self.y, width, self.height) - - def left_side(self, width): - """Create a new LayoutRect from the left side of this one.""" - return LayoutRect(self.x, self.y, width, self.height) - - def top_side(self, height): - """Create a new LayoutRect from the top side of this one.""" - return LayoutRect(self.x, self.y, self.width, height) - - def bottom_side(self, height): - """Create a new LayoutRect from the bottom side of this one.""" - return LayoutRect(self.x, self.bottom - height, self.width, height) - - def past_right(self, width): - """Create a LayoutRect width pixels to the right of this one>""" - return LayoutRect(self.right, self.y, width, self.height) - - def past_left(self, width): - """Create a LayoutRect width pixels to the right of this one>""" - return LayoutRect(self.x-width, self.y, width, self.height) - - def past_top(self, height): - """Create a LayoutRect height pixels above this one>""" - return LayoutRect(self.x, self.y-height, self.width, height) - - def past_bottom(self, height): - """Create a LayoutRect height pixels below this one>""" - return LayoutRect(self.x, self.bottom, self.width, height) - - def is_point_inside(self, x, y): - return (self.x <= x < self.x + self.width - and self.y <= y < self.y + self.height) - - def get_right(self): - return self.x + self.width - def set_right(self, right): - self.width = right - self.x - right = property(get_right, set_right) - - def get_bottom(self): - return self.y + self.height - def set_bottom(self, bottom): - self.height = bottom - self.y - bottom = property(get_bottom, set_bottom) - -class Layout(object): - """Store the layout for a cell - - Layouts are lightweight objects that keep track of where stuff is inside a - cell. They can be used for both rendering and hotspot tracking. - - :attribute last_rect: the LayoutRect most recently added to the layout - """ - - def __init__(self): - self._rects = [] - self.last_rect = None - - def rect_count(self): - """Get the number of rects in this layout.""" - return len(self._rects) - - def add(self, x, y, width, height, drawing_function=None, - hotspot=None): - """Add a new element to this Layout - - :param x: x coordinate - :param y: y coordinate - :param width: width - :param height: height - :param drawing_function: if set, call this function to render the - element on a DrawingContext - :param hotspot: if set, the hotspot for this element - - :returns: LayoutRect of the added element - """ - return self.add_rect(LayoutRect(x, y, width, height), - drawing_function, hotspot) - - def add_rect(self, layout_rect, drawing_function=None, hotspot=None): - """Add a new element to this Layout using a LayoutRect - - :param layout_rect: LayoutRect object for positioning - :param drawing_function: if set, call this function to render the - element on a DrawingContext - :param hotspot: if set, the hotspot for this element - :returns: LayoutRect of the added element - """ - self.last_rect = layout_rect - value = (layout_rect, drawing_function, hotspot) - self._rects.append(value) - return layout_rect - - def add_text_line(self, textbox, x, y, width, hotspot=None): - """Add one line of text from a text box to the layout - - This is convenience method that's equivelent to: - self.add(x, y, width, textbox.font.line_height(), textbox.draw, - hotspot) - """ - return self.add(x, y, width, textbox.font.line_height(), textbox.draw, - hotspot) - - def add_image(self, image, x, y, hotspot=None): - """Add an ImageSurface to the layout - - This is convenience method that's equivelent to: - self.add(x, y, image.width, image.height, image.draw, hotspot) - """ - width, height = image.get_size() - return self.add(x, y, width, height, image.draw, hotspot) - - def merge(self, layout): - """Add another layout's elements with this one - """ - self._rects.extend(layout._rects) - self.last_rect = layout.last_rect - - def translate(self, delta_x, delta_y): - """Move each element inside this layout """ - for rect, _, _ in self._rects: - rect.x += delta_x - rect.y += delta_y - - def max_width(self): - """Get the max width of the elements in current group.""" - return max(rect.width for (rect, _, _) in self._rects) - - def max_height(self): - """Get the max height of the elements in current group.""" - return max(rect.height for (rect, _, _) in self._rects) - - def center_x(self, left=None, right=None): - """Center each rect inside this layout horizontally. - - The left and right arguments control the area to center the rects to. - If one is missing, it will be calculated using largest width of the - layout. If both are missing, a ValueError will be thrown. - - :param left: left-side of the area to center to - :param right: right-side of the area to center to - """ - if left is None: - if right is None: - raise ValueError("both left and right are None") - left = right - self.max_width() - elif right is None: - right = left + self.max_width() - area_width = right - left - for rect, _, _ in self._rects: - rect.x = left + (area_width - rect.width) // 2 - - def center_y(self, top=None, bottom=None): - """Center each rect inside this layout vertically. - - The top and bottom arguments control the area to center the rects to. - If one is missing, it will be calculated using largest height in the - layout. If both are missing, a ValueError will be thrown. - - :param top: top of the area to center to - :param bottom: bottom of the area to center to - """ - if top is None: - if bottom is None: - raise ValueError("both top and bottom are None") - top = bottom - self.max_height() - elif bottom is None: - bottom = top + self.max_height() - area_height = bottom - top - for rect, _, _ in self._rects: - rect.y = top + (area_height - rect.height) // 2 - - def find_hotspot(self, x, y): - """Find a hotspot inside our rects. - - If (x, y) is inside any of the rects for this layout and that rect has - a hotspot set, a 3-tuple containing the hotspot name, and the x, y - coordinates relative to the hotspot rect. If no rect is found, we - return None. - - :param x: x coordinate to check - :param y: y coordinate to check - """ - for rect, drawing_function, hotspot in self._rects: - if hotspot is not None and rect.is_point_inside(x, y): - return hotspot, x - rect.x, y - rect.y - return None - - def draw(self, context): - """Render each layout rect onto context - - :param context: a DrawingContext to draw on - """ - - for rect, drawing_function, hotspot in self._rects: - if drawing_function is not None: - drawing_function(context, rect.x, rect.y, rect.width, - rect.height) diff --git a/mvc/widgets/dialogs.py b/mvc/widgets/dialogs.py deleted file mode 100644 index 3ccdcd7..0000000 --- a/mvc/widgets/dialogs.py +++ /dev/null @@ -1,276 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -"""``miro.frontends.widgets.dialogs`` -- Dialog boxes for the Widget -frontend. - -The difference between this module and rundialog.py is that rundialog -handles dialog boxes that are coming from the backend code. This -model handles dialogs that we create from the frontend - -One big difference is that we don't have to be as general about -dialogs, so they can present a somewhat nicer API. One important -difference is that all of the dialogs run modally. -""" - -from mvc.widgets import widgetset -from mvc.widgets import widgetutil - -class DialogButton(object): - def __init__(self, text): - self._text = text - def __eq__(self, other): - return isinstance(other, DialogButton) and self.text == other.text - def __str__(self): - return "DialogButton(%r)" % self.text - @property - def text(self): - return unicode(self._text) - -BUTTON_OK = DialogButton("OK") -BUTTON_APPLY = DialogButton("Apply") -BUTTON_CLOSE = DialogButton("Close") -BUTTON_CANCEL = DialogButton("Cancel") -BUTTON_DONE = DialogButton("Done") -BUTTON_YES = DialogButton("Yes") -BUTTON_NO = DialogButton("No") -BUTTON_QUIT = DialogButton("Quit") -BUTTON_CONTINUE = DialogButton("Continue") -BUTTON_IGNORE = DialogButton("Ignore") -BUTTON_IMPORT_FILES = DialogButton("Import Files") -BUTTON_SUBMIT_REPORT = DialogButton("Submit Crash Report") -BUTTON_MIGRATE = DialogButton("Migrate") -BUTTON_DONT_MIGRATE = DialogButton("Don't Migrate") -BUTTON_DOWNLOAD = DialogButton("Download") -BUTTON_REMOVE_ENTRY = DialogButton("Remove Entry") -BUTTON_DELETE_FILE = DialogButton("Delete File") -BUTTON_DELETE_FILES = DialogButton("Delete Files") -BUTTON_KEEP_VIDEOS = DialogButton("Keep Videos") -BUTTON_DELETE_VIDEOS = DialogButton("Delete Videos") -BUTTON_CREATE = DialogButton("Create") -BUTTON_CREATE_FEED = DialogButton("Create Podcast") -BUTTON_CREATE_FOLDER = DialogButton("Create Folder") -BUTTON_CHOOSE_NEW_FOLDER = DialogButton("Choose New Folder") -BUTTON_ADD_FOLDER = DialogButton("Add Folder") -BUTTON_ADD = DialogButton("Add") -BUTTON_ADD_INTO_NEW_FOLDER = DialogButton("Add Into New Folder") -BUTTON_KEEP = DialogButton("Keep") -BUTTON_DELETE = DialogButton("Delete") -BUTTON_REMOVE = DialogButton("Remove") -BUTTON_NOT_NOW = DialogButton("Not Now") -BUTTON_CLOSE_TO_TRAY = DialogButton("Close to Tray") -BUTTON_LAUNCH_MIRO = DialogButton("Launch Miro") -BUTTON_DOWNLOAD_ANYWAY = DialogButton("Download Anyway") -BUTTON_OPEN_IN_EXTERNAL_BROWSER = DialogButton( - "Open in External Browser") -BUTTON_DONT_INSTALL = DialogButton("Don't Install") -BUTTON_SUBSCRIBE = DialogButton("Subscribe") -BUTTON_STOP_WATCHING = DialogButton("Stop Watching") -BUTTON_RETRY = DialogButton("Retry") -BUTTON_START_FRESH = DialogButton("Start Fresh") -BUTTON_INCLUDE_DATABASE = DialogButton("Include Database") -BUTTON_DONT_INCLUDE_DATABASE = DialogButton( - "Don't Include Database") - -WARNING_MESSAGE = 0 -INFO_MESSAGE = 1 -CRITICAL_MESSAGE = 2 - - -class ProgressDialog(widgetset.Dialog): - def __init__(self, title): - widgetset.Dialog.__init__(self, title, description='') - self.progress_bar = widgetset.ProgressBar() - self.label = widgetset.Label() - self.label.set_size(1.2) - self.vbox = widgetset.VBox(spacing=6) - self.vbox.pack_end(widgetutil.align_center(self.label)) - self.vbox.pack_end(self.progress_bar) - self.set_extra_widget(self.vbox) - - def update(self, description, progress): - self.label.set_text(description) - if progress >= 0: - self.progress_bar.set_progress(progress) - self.progress_bar.stop_pulsing() - else: - self.progress_bar.start_pulsing() - -class DBUpgradeProgressDialog(widgetset.Dialog): - def __init__(self, title, text): - widgetset.Dialog.__init__(self, title) - self.progress_bar = widgetset.ProgressBar() - self.top_label = widgetset.Label() - self.top_label.set_text(text) - self.top_label.set_wrap(True) - self.top_label.set_size_request(350, -1) - self.label = widgetset.Label() - self.vbox = widgetset.VBox(spacing=6) - self.vbox.pack_end(widgetutil.align_center(self.label)) - self.vbox.pack_end(self.progress_bar) - self.vbox.pack_end(widgetutil.pad(self.top_label, bottom=6)) - self.set_extra_widget(self.vbox) - - def update(self, stage, stage_progress, progress): - self.label.set_text(stage) - self.progress_bar.set_progress(progress) - -def show_about(): - window = widgetset.AboutDialog() - set_transient_for_main(window) - try: - window.run() - finally: - window.destroy() - -def show_message(title, description, alert_type=INFO_MESSAGE, - transient_for=None): - """Display a message to the user and wait for them to click OK""" - window = widgetset.AlertDialog(title, description, alert_type) - _set_transient_for(window, transient_for) - try: - window.add_button(BUTTON_OK.text) - window.run() - finally: - window.destroy() - -def show_choice_dialog(title, description, choices, transient_for=None): - """Display a message to the user and wait for them to choose an option. - Returns the button object chosen.""" - window = widgetset.Dialog(title, description) - try: - for mem in choices: - window.add_button(mem.text) - response = window.run() - return choices[response] - finally: - window.destroy() - -def ask_for_string(title, description, initial_text=None, transient_for=None): - """Ask the user to enter a string in a TextEntry box. - - description - textual description with newlines - initial_text - None, string or callable to pre-populate the entry box - - Returns the value entered, or None if the user clicked cancel - """ - window = widgetset.Dialog(title, description) - try: - window.add_button(BUTTON_OK.text) - window.add_button(BUTTON_CANCEL.text) - entry = widgetset.TextEntry() - entry.set_activates_default(True) - if initial_text: - if callable(initial_text): - initial_text = initial_text() - entry.set_text(initial_text) - window.set_extra_widget(entry) - response = window.run() - if response == 0: - return entry.get_text() - else: - return None - finally: - window.destroy() - -def ask_for_choice(title, description, choices): - """Ask the user to enter a string in a TextEntry box. - - :param title: title for the window - :param description: textual description with newlines - :param choices: list of labels for choices - Returns the index of the value chosen, or None if the user clicked cancel - """ - window = widgetset.Dialog(title, description) - try: - window.add_button(BUTTON_OK.text) - window.add_button(BUTTON_CANCEL.text) - menu = widgetset.OptionMenu(choices) - window.set_extra_widget(menu) - response = window.run() - if response == 0: - return menu.get_selected() - else: - return None - finally: - window.destroy() - -def ask_for_open_pathname(title, initial_filename=None, filters=[], - transient_for=None, select_multiple=False): - """Returns the file pathname or None. - """ - window = widgetset.FileOpenDialog(title) - _set_transient_for(window, transient_for) - try: - if initial_filename: - window.set_filename(initial_filename) - - if filters: - window.add_filters(filters) - - if select_multiple: - window.set_select_multiple(select_multiple) - - response = window.run() - if response == 0: - if select_multiple: - return window.get_filenames() - else: - return window.get_filename() - finally: - window.destroy() - -def ask_for_save_pathname(title, initial_filename=None, transient_for=None): - """Returns the file pathname or None. - """ - window = widgetset.FileSaveDialog(title) - _set_transient_for(window, transient_for) - try: - if initial_filename: - window.set_filename(initial_filename) - response = window.run() - if response == 0: - return window.get_filename() - finally: - window.destroy() - -def ask_for_directory(title, initial_directory=None, transient_for=None): - """Returns the directory pathname or None. - """ - window = widgetset.DirectorySelectDialog(title) - _set_transient_for(window, transient_for) - try: - if initial_directory: - window.set_directory(initial_directory) - - response = window.run() - if response == 0: - return window.get_directory() - finally: - window.destroy() diff --git a/mvc/widgets/gtk/__init__.py b/mvc/widgets/gtk/__init__.py deleted file mode 100644 index 8e58700..0000000 --- a/mvc/widgets/gtk/__init__.py +++ /dev/null @@ -1,65 +0,0 @@ -import os -import sys -import gtk -import gobject - -def initialize(app): - from gtkmenus import MainWindowMenuBar - app.menubar = MainWindowMenuBar() - app.startup() - app.run() - -def attach_menubar(): - from mvc.widgets import app - app.widgetapp.vbox.pack_start(app.widgetapp.menubar) - -def mainloop_start(): - gobject.threads_init() - gtk.main() - -def mainloop_stop(): - gtk.main_quit() - -def idle_add(callback, periodic=None): - if periodic is not None and periodic < 0: - raise ValueError('periodic cannot be negative') - def wrapper(): - callback() - return periodic is not None - delay = periodic - if delay is not None: - delay *= 1000 # milliseconds - else: - delay = 0 - return gobject.timeout_add(delay, wrapper) - -def idle_remove(id_): - gobject.source_remove(id_) - -def check_kde(): - return os.environ.get("KDE_FULL_SESSION", None) != None - -def open_file_linux(filename): - if check_kde(): - os.spawnlp(os.P_NOWAIT, "kfmclient", "kfmclient", # kfmclient is part of konqueror - "exec", "file://" + filename) - else: - os.spawnlp(os.P_NOWAIT, "gnome-open", "gnome-open", filename) - -def reveal_file(filename): - if hasattr(os, 'startfile'): # Windows - os.startfile(os.path.dirname(filename)) - else: - open_file_linux(filename) - -def get_conversion_directory_windows(): - from mvc.windows import specialfolders - return specialfolders.base_movies_directory - -def get_conversion_directory_linux(): - return os.path.expanduser('~') - -if sys.platform == 'win32': - get_conversion_directory = get_conversion_directory_windows -else: - get_conversion_directory = get_conversion_directory_linux diff --git a/mvc/widgets/gtk/base.py b/mvc/widgets/gtk/base.py deleted file mode 100644 index e02db3f..0000000 --- a/mvc/widgets/gtk/base.py +++ /dev/null @@ -1,300 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -""".base -- Base classes for GTK Widgets.""" - -import gtk - -from mvc import signals -import wrappermap -from .weakconnect import weak_connect -import keymap - -def make_gdk_color(miro_color): - def convert_value(value): - return int(round(value * 65535)) - - values = tuple(convert_value(c) for c in miro_color) - return gtk.gdk.Color(*values) - -class Widget(signals.SignalEmitter): - """Base class for GTK widgets. - - The actual GTK Widget is stored in '_widget'. - - signals: - - 'size-allocated' (widget, width, height): The widget had it's size - allocated. - """ - def __init__(self, *signal_names): - signals.SignalEmitter.__init__(self, *signal_names) - self.create_signal('size-allocated') - self.create_signal('key-press') - self.create_signal('focus-out') - self.style_mods = {} - self.use_custom_style = False - self._disabled = False - - def wrapped_widget_connect(self, signal, method, *user_args): - """Connect to a signal of the widget we're wrapping. - - We use a weak reference to ensures that we don't have circular - references between the wrapped widget and the wrapper widget. - """ - return weak_connect(self._widget, signal, method, *user_args) - - def set_widget(self, widget): - self._widget = widget - wrappermap.add(self._widget, self) - if self.should_connect_to_hierarchy_changed(): - self.wrapped_widget_connect('hierarchy_changed', - self.on_hierarchy_changed) - self.wrapped_widget_connect('size-allocate', self.on_size_allocate) - self.wrapped_widget_connect('key-press-event', self.on_key_press) - self.wrapped_widget_connect('focus-out-event', self.on_focus_out) - self.use_custom_style_callback = None - - def should_connect_to_hierarchy_changed(self): - # GTK creates windows to handle submenus, which messes with our - # on_hierarchy_changed callback. We don't care about custom styles - # for menus anyways, so just ignore the signal. - return not isinstance(self._widget, gtk.MenuItem) - - def set_can_focus(self, allow): - """Set if we allow the widget to hold keyboard focus. - """ - if allow: - self._widget.set_flags(gtk.CAN_FOCUS) - else: - self._widget.unset_flags(gtk.CAN_FOCUS) - - def on_hierarchy_changed(self, widget, previous_toplevel): - toplevel = widget.get_toplevel() - if not (toplevel.flags() & gtk.TOPLEVEL): - toplevel = None - if previous_toplevel != toplevel: - if self.use_custom_style_callback: - old_window = wrappermap.wrapper(previous_toplevel) - old_window.disconnect(self.use_custom_style_callback) - if toplevel is not None: - window = wrappermap.wrapper(toplevel) - callback_id = window.connect('use-custom-style-changed', - self.on_use_custom_style_changed) - self.use_custom_style_callback = callback_id - else: - self.use_custom_style_callback = None - if previous_toplevel is None: - # Setup our initial state - self.on_use_custom_style_changed(window) - - def on_size_allocate(self, widget, allocation): - self.emit('size-allocated', allocation.width, allocation.height) - - def on_key_press(self, widget, event): - key_modifiers = keymap.translate_gtk_event(event) - if key_modifiers: - key, modifiers = key_modifiers - return self.emit('key-press', key, modifiers) - - def on_focus_out(self, widget, event): - self.emit('focus-out') - - def on_use_custom_style_changed(self, window): - self.use_custom_style = window.use_custom_style - if not self.style_mods: - return # no need to do any work here - if self.use_custom_style: - for (what, state), color in self.style_mods.items(): - self.do_modify_style(what, state, color) - else: - # This should reset the style changes we've made - self._widget.modify_style(gtk.RcStyle()) - self.handle_custom_style_change() - - def handle_custom_style_change(self): - """Called when the user changes a from a theme where we don't want to - use our custom style to one where we do, or vice-versa. The Widget - class handles changes that used modify_style(), but subclasses might - want to do additional work. - """ - pass - - def modify_style(self, what, state, color): - """Change the style of our widget. This method checks to see if we - think the user's theme is compatible with our stylings, and doesn't - change things if not. what is either 'base', 'text', 'bg' or 'fg' - depending on which color is to be changed. - """ - if self.use_custom_style: - self.do_modify_style(what, state, color) - self.style_mods[(what, state)] = color - - def unmodify_style(self, what, state): - if (what, state) in self.style_mods: - del self.style_mods[(what, state)] - default_color = getattr(self.style, what)[state] - self.do_modify_style(what, state, default_color) - - def do_modify_style(self, what, state, color): - if what == 'base': - self._widget.modify_base(state, color) - elif what == 'text': - self._widget.modify_text(state, color) - elif what == 'bg': - self._widget.modify_bg(state, color) - elif what == 'fg': - self._widget.modify_fg(state, color) - else: - raise ValueError("Unknown what in do_modify_style: %s" % what) - - def get_window(self): - gtk_window = self._widget.get_toplevel() - return wrappermap.wrapper(gtk_window) - - def clear_size_request_cache(self): - # This is just an OS X hack - pass - - def get_size_request(self): - return self._widget.size_request() - - def invalidate_size_request(self): - self._widget.queue_resize() - - def set_size_request(self, width, height): - if not width >= -1 and height >= -1: - raise ValueError("invalid dimensions in set_size_request: %s" % - repr((width, height))) - self._widget.set_size_request(width, height) - - def relative_position(self, other_widget): - return other_widget._widget.translate_coordinates(self._widget, 0, 0) - - def convert_gtk_color(self, color): - return (color.red / 65535.0, color.green / 65535.0, - color.blue / 65535.0) - - def get_width(self): - try: - return self._widget.allocation.width - except AttributeError: - return -1 - width = property(get_width) - - def get_height(self): - try: - return self._widget.allocation.height - except AttributeError: - return -1 - height = property(get_height) - - def queue_redraw(self): - if self._widget: - self._widget.queue_draw() - - def redraw_now(self): - if self._widget: - self._widget.queue_draw() - self._widget.window.process_updates(True) - - def forward_signal(self, signal_name, forwarded_signal_name=None): - """Add a callback so that when the GTK widget emits a signal, we emit - signal from the wrapper widget. - """ - if forwarded_signal_name is None: - forwarded_signal_name = signal_name - self.wrapped_widget_connect(signal_name, self.do_forward_signal, - forwarded_signal_name) - - def do_forward_signal(self, widget, *args): - forwarded_signal_name = args[-1] - args = args[:-1] - self.emit(forwarded_signal_name, *args) - - def make_color(self, miro_color): - color = make_gdk_color(miro_color) - self._widget.get_colormap().alloc_color(color) - return color - - def enable(self): - self._disabled = False - self._widget.set_sensitive(True) - - def disable(self): - self._disabled = True - self._widget.set_sensitive(False) - - def set_disabled(self, disabled): - if disabled: - self.disable() - else: - self.enable() - - def get_disabled(self): - return self._disabled - -class Bin(Widget): - def __init__(self): - Widget.__init__(self) - self.child = None - - def add(self, child): - if self.child is not None: - raise ValueError("Already have a child: %s" % self.child) - if child._widget.parent is not None: - raise ValueError("%s already has a parent" % child) - self.child = child - self.add_child_to_widget() - child._widget.show() - - def add_child_to_widget(self): - self._widget.add(self.child._widget) - - def remove_child_from_widget(self): - if self._widget.get_child() is not None: - # otherwise gtkmozembed gets confused - self._widget.get_child().hide() - self._widget.remove(self._widget.get_child()) - - - def remove(self): - if self.child is not None: - self.child = None - self.remove_child_from_widget() - - def set_child(self, new_child): - self.remove() - self.add(new_child) - - def enable(self): - self.child.enable() - - def disable(self): - self.child.disable() diff --git a/mvc/widgets/gtk/const.py b/mvc/widgets/gtk/const.py deleted file mode 100644 index 5e9ec05..0000000 --- a/mvc/widgets/gtk/const.py +++ /dev/null @@ -1,44 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -""".const -- Constants.""" - -import gtk - -DRAG_ACTION_NONE = 0 -DRAG_ACTION_COPY = gtk.gdk.ACTION_COPY -DRAG_ACTION_MOVE = gtk.gdk.ACTION_MOVE -DRAG_ACTION_LINK = gtk.gdk.ACTION_LINK -DRAG_ACTION_ALL = DRAG_ACTION_COPY | DRAG_ACTION_MOVE | DRAG_ACTION_LINK - -ITEM_TITLE_FONT = "Helvetica" -ITEM_DESC_FONT = "Helvetica" -ITEM_INFO_FONT = "Lucida Grande" - -TOOLBAR_GRAY = (0.2, 0.2, 0.2) diff --git a/mvc/widgets/gtk/contextmenu.py b/mvc/widgets/gtk/contextmenu.py deleted file mode 100644 index cd5b6ba..0000000 --- a/mvc/widgets/gtk/contextmenu.py +++ /dev/null @@ -1,31 +0,0 @@ -import gtk - -from .base import Widget - -class ContextMenu(Widget): - - def __init__(self, options): - super(ContextMenu, self).__init__() - self.set_widget(gtk.Menu()) - for i, item_info in enumerate(options): - if item_info is None: - # separator - item = gtk.SeparatorMenuItem() - else: - label, callback = item_info - item = gtk.MenuItem(label) - if isinstance(callback, list): - submenu = ContextMenu(callback) - item.set_submenu(submenu._widget) - elif callback is not None: - item.connect('activate', self.on_activate, callback, i) - else: - item.set_sensitive(False) - self._widget.append(item) - item.show() - - def popup(self): - self._widget.popup(None, None, None, 0, 0) - - def on_activate(self, widget, callback, i): - callback(self, i) diff --git a/mvc/widgets/gtk/controls.py b/mvc/widgets/gtk/controls.py deleted file mode 100644 index 4367c1f..0000000 --- a/mvc/widgets/gtk/controls.py +++ /dev/null @@ -1,337 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -""".controls -- Control Widgets.""" - -import gtk -import pango - -from mvc.widgets import widgetconst -import layout -from .base import Widget -from .simple import Label - -class BinBaselineCalculator(object): - """Mixin class that defines the baseline method for gtk.Bin subclasses, - where the child is the label that we are trying to get the baseline for. - """ - - def baseline(self): - my_size = self._widget.size_request() - child_size = self._widget.child.size_request() - ypad = (my_size[1] - child_size[1]) / 2 - - pango_context = self._widget.get_pango_context() - metrics = pango_context.get_metrics(self._widget.style.font_desc) - return pango.PIXELS(metrics.get_descent()) + ypad - -class TextEntry(Widget): - entry_class = gtk.Entry - def __init__(self, initial_text=None): - Widget.__init__(self) - self.create_signal('activate') - self.create_signal('changed') - self.create_signal('validate') - self.set_widget(self.entry_class()) - self.forward_signal('activate') - self.forward_signal('changed') - if initial_text is not None: - self.set_text(initial_text) - - def focus(self): - self._widget.grab_focus() - - def start_editing(self, text): - self.set_text(text) - self.focus() - self._widget.emit('move-cursor', gtk.MOVEMENT_BUFFER_ENDS, 1, False) - - def set_text(self, text): - self._widget.set_text(text) - - def get_text(self): - return self._widget.get_text().decode('utf-8') - - def set_max_length(self, chars): - self._widget.set_max_length(chars) - - def set_width(self, chars): - self._widget.set_width_chars(chars) - - def set_invisible(self, setting): - self._widget.props.visibility = not setting - - def set_activates_default(self, setting): - self._widget.set_activates_default(setting) - - def baseline(self): - layout_height = pango.PIXELS(self._widget.get_layout().get_size()[1]) - ypad = (self._widget.size_request()[1] - layout_height) / 2 - pango_context = self._widget.get_pango_context() - metrics = pango_context.get_metrics(self._widget.style.font_desc) - return pango.PIXELS(metrics.get_descent()) + ypad - - -class NumberEntry(TextEntry): - def __init__(self, initial_text=None): - TextEntry.__init__(self, initial_text) - self._widget.connect('changed', self.validate) - self.previous_text = initial_text or "" - - def validate(self, entry): - text = self.get_text() - if text.isdigit() or not text: - self.previous_text = text - else: - self._widget.set_text(self.previous_text) - -class SecureTextEntry(TextEntry): - def __init__(self, initial_text=None): - TextEntry.__init__(self, initial_text) - self.set_invisible(True) - -class MultilineTextEntry(Widget): - entry_class = gtk.TextView - def __init__(self, initial_text=None, border=False): - Widget.__init__(self) - self.set_widget(self.entry_class()) - if initial_text is not None: - self.set_text(initial_text) - self._widget.set_wrap_mode(gtk.WRAP_WORD) - self._widget.set_accepts_tab(False) - self.border = border - - def focus(self): - self._widget.grab_focus() - - def set_text(self, text): - self._widget.get_buffer().set_text(text) - - def get_text(self): - buffer_ = self._widget.get_buffer() - return buffer_.get_text(*(buffer_.get_bounds())).decode('utf-8') - - def baseline(self): - # FIXME - layout_height = pango.PIXELS(self._widget.get_layout().get_size()[1]) - ypad = (self._widget.size_request()[1] - layout_height) / 2 - pango_context = self._widget.get_pango_context() - metrics = pango_context.get_metrics(self._widget.style.font_desc) - return pango.PIXELS(metrics.get_descent()) + ypad - - def set_editable(self, editable): - self._widget.set_editable(editable) - -class Checkbox(Widget, BinBaselineCalculator): - """Widget that the user can toggle on or off.""" - - def __init__(self, text=None, bold=False, color=None): - Widget.__init__(self) - BinBaselineCalculator.__init__(self) - if text is None: - text = '' - self.set_widget(gtk.CheckButton()) - self.label = Label(text, color=color) - self._widget.add(self.label._widget) - self.label._widget.show() - self.create_signal('toggled') - self.forward_signal('toggled') - if bold: - self.label.set_bold(True) - - def get_checked(self): - return self._widget.get_active() - - def set_checked(self, value): - self._widget.set_active(value) - - def set_size(self, scale_factor): - self.label.set_size(scale_factor) - - def get_text_padding(self): - """ - Returns the amount of space the checkbox takes up before the label. - """ - indicator_size = self._widget.style_get_property('indicator-size') - indicator_spacing = self._widget.style_get_property( - 'indicator-spacing') - focus_width = self._widget.style_get_property('focus-line-width') - focus_padding = self._widget.style_get_property('focus-padding') - return (indicator_size + 3 * indicator_spacing + 2 * (focus_width + - focus_padding)) - -class RadioButtonGroup(Widget, BinBaselineCalculator): - """RadioButtonGroup. - - Create the group, then create a bunch of RadioButtons passing in the group. - - NB: GTK has built-in radio button grouping functionality, and we should - be using that but we need this widget for portable code. We create - a dummy GTK radio button and make this the "root" button which gets - inherited by all buttons in this radio button group. - """ - def __init__(self): - Widget.__init__(self) - BinBaselineCalculator.__init__(self) - self.set_widget(gtk.RadioButton(label="")) - self._widget.set_active(False) - self._buttons = [] - - def add_button(self, button): - self._buttons.append(button) - - def get_buttons(self): - return self._buttons - - def get_selected(self): - for mem in self._buttons: - if mem.get_selected(): - return mem - - def set_selected(self, button): - for mem in self._buttons: - if mem is button: - mem._widget.set_active(True) - else: - mem._widget.set_active(False) - -class RadioButton(Widget, BinBaselineCalculator): - """RadioButton.""" - def __init__(self, label, group=None, color=None): - Widget.__init__(self) - BinBaselineCalculator.__init__(self) - if group: - self.group = group - else: - self.group = RadioButtonGroup() - self.set_widget(gtk.RadioButton(group=self.group._widget)) - self.label = Label(label, color=color) - self._widget.add(self.label._widget) - self.label._widget.show() - self.create_signal('clicked') - self.forward_signal('clicked') - - group.add_button(self) - - def set_size(self, size): - self.label.set_size(size) - - def get_group(self): - return self.group - - def get_selected(self): - return self._widget.get_active() - - def set_selected(self): - self.group.set_selected(self) - -class Button(Widget, BinBaselineCalculator): - def __init__(self, text, style='normal', width=None): - Widget.__init__(self) - BinBaselineCalculator.__init__(self) - # We just ignore style here, GTK users expect their own buttons. - self.set_widget(gtk.Button()) - self.create_signal('clicked') - self.forward_signal('clicked') - self.label = Label(text) - # only honor width if its bigger than the width we need to display the - # label (#18994) - if width and width > self.label.get_width(): - alignment = layout.Alignment(0.5, 0.5, 0, 0) - alignment.set_size_request(width, -1) - alignment.add(self.label) - self._widget.add(alignment._widget) - else: - self._widget.add(self.label._widget) - self.label._widget.show() - - def set_text(self, title): - self.label.set_text(title) - - def set_bold(self, bold): - self.label.set_bold(bold) - - def set_size(self, scale_factor): - self.label.set_size(scale_factor) - - def set_color(self, color): - self.label.set_color(color) - -class OptionMenu(Widget): - def __init__(self, options): - Widget.__init__(self) - self.create_signal('changed') - - self.set_widget(gtk.ComboBox(gtk.ListStore(str, str))) - self.cell = gtk.CellRendererText() - self._widget.pack_start(self.cell, True) - self._widget.add_attribute(self.cell, 'text', 0) - if options: - for option, value in options: - self._widget.get_model().append((option, value)) - self._widget.set_active(0) - self.options = options - self.wrapped_widget_connect('changed', self.on_changed) - - def baseline(self): - my_size = self._widget.size_request() - child_size = self._widget.child.size_request() - ypad = self.cell.props.ypad + (my_size[1] - child_size[1]) / 2 - - pango_context = self._widget.get_pango_context() - metrics = pango_context.get_metrics(self._widget.style.font_desc) - return pango.PIXELS(metrics.get_descent()) + ypad - - def set_bold(self, bold): - if bold: - self.cell.props.weight = pango.WEIGHT_BOLD - else: - self.cell.props.weight = pango.WEIGHT_NORMAL - - def set_size(self, size): - if size == widgetconst.SIZE_NORMAL: - self.cell.props.scale = 1 - else: - self.cell.props.scale = 0.75 - - def set_color(self, color): - self.cell.props.foreground_gdk = self.make_color(color) - - def set_selected(self, index): - self._widget.set_active(index) - - def get_selected(self): - return self._widget.get_active() - - def on_changed(self, widget): - index = widget.get_active() - self.emit('changed', index) - - def set_width(self, width): - self._widget.set_property('width-request', width) diff --git a/mvc/widgets/gtk/customcontrols.py b/mvc/widgets/gtk/customcontrols.py deleted file mode 100644 index 070cebd..0000000 --- a/mvc/widgets/gtk/customcontrols.py +++ /dev/null @@ -1,517 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -""".controls -- Contains the ControlBox and -CustomControl classes. These handle the custom buttons/sliders used during -playback. -""" - -from __future__ import division -import math - -import gtk -import gobject - -import wrappermap -from .base import Widget -from .simple import Label, Image -from .drawing import (CustomDrawingMixin, Drawable, - ImageSurface) -from mvc.widgets import widgetconst - -class CustomControlMixin(CustomDrawingMixin): - def do_expose_event(self, event): - CustomDrawingMixin.do_expose_event(self, event) - if self.is_focus(): - style = self.get_style() - style.paint_focus(self.window, self.state, - event.area, self, None, self.allocation.x, - self.allocation.y, self.allocation.width, - self.allocation.height) - -class CustomButtonWidget(CustomControlMixin, gtk.Button): - def draw(self, wrapper, context): - if self.is_active(): - wrapper.state = 'pressed' - elif self.state == gtk.STATE_PRELIGHT: - wrapper.state = 'hover' - else: - wrapper.state = 'normal' - wrapper.draw(context, wrapper.layout_manager) - self.set_focus_on_click(False) - - def is_active(self): - return self.state == gtk.STATE_ACTIVE - -class ContinuousCustomButtonWidget(CustomButtonWidget): - def is_active(self): - return (self.state == gtk.STATE_ACTIVE or - wrappermap.wrapper(self).button_down) - -class DragableCustomButtonWidget(CustomButtonWidget): - def __init__(self): - CustomButtonWidget.__init__(self) - self.button_press_x = None - self.set_events(self.get_events() | gtk.gdk.POINTER_MOTION_MASK) - - def do_button_press_event(self, event): - self.button_press_x = event.x - self.last_drag_event = None - gtk.Button.do_button_press_event(self, event) - - def do_button_release_event(self, event): - self.button_press_x = None - gtk.Button.do_button_release_event(self, event) - - def do_motion_notify_event(self, event): - DRAG_THRESHOLD = 15 - if self.button_press_x is None: - # button not down - return - if (self.last_drag_event != 'right' and - event.x > self.button_press_x + DRAG_THRESHOLD): - wrappermap.wrapper(self).emit('dragged-right') - self.last_drag_event = 'right' - elif (self.last_drag_event != 'left' and - event.x < self.button_press_x - DRAG_THRESHOLD): - wrappermap.wrapper(self).emit('dragged-left') - self.last_drag_event = 'left' - - def do_clicked(self): - # only emit clicked if we didn't emit dragged-left or dragged-right - if self.last_drag_event is None: - wrappermap.wrapper(self).emit('clicked') - -class _DragInfo(object): - """Info about the start of a drag. - - Attributes: - - - button: button that started the drag - - start_pos: position of the slider - - click_pos: position of the click - - Note that start_pos and click_pos will be different if the user clicks - inside the slider. - """ - - def __init__(self, button, start_pos, click_pos): - self.button = button - self.start_pos = start_pos - self.click_pos = click_pos - -class CustomScaleMixin(CustomControlMixin): - def __init__(self): - CustomControlMixin.__init__(self) - self.drag_info = None - self.min = self.max = 0.0 - - def get_range(self): - return self.min, self.max - - def set_range(self, min, max): - self.min = float(min) - self.max = float(max) - gtk.Range.set_range(self, min, max) - - def is_continuous(self): - return wrappermap.wrapper(self).is_continuous() - - def is_horizontal(self): - # this comes from a mixin - pass - - def gtk_scale_class(self): - if self.is_horizontal(): - return gtk.HScale - else: - return gtk.VScale - - def get_slider_pos(self, value=None): - if value is None: - value = self.get_value() - if self.is_horizontal(): - size = self.allocation.width - else: - size = self.allocation.height - ratio = (float(value) - self.min) / (self.max - self.min) - start_pos = self.slider_size() / 2.0 - return start_pos + ratio * (size - self.slider_size()) - - def slider_size(self): - return wrappermap.wrapper(self).slider_size() - - def _event_pos(self, event): - """Get the position of an event. - - If we are horizontal, this will be the x coordinate. If we are - vertical, the y. - """ - if self.is_horizontal(): - return event.x - else: - return event.y - - def do_button_press_event(self, event): - if self.drag_info is not None: - return - current_pos = self.get_slider_pos() - event_pos = self._event_pos(event) - pos_difference = abs(current_pos - event_pos) - # only move the slider if the click was outside its boundaries - # (#18840) - if pos_difference > self.slider_size() / 2.0: - self.move_slider(event_pos) - current_pos = event_pos - self.drag_info = _DragInfo(event.button, current_pos, event_pos) - self.grab_focus() - wrappermap.wrapper(self).emit('pressed') - - def do_motion_notify_event(self, event): - if self.drag_info is not None: - event_pos = self._event_pos(event) - delta = event_pos - self.drag_info.click_pos - self.move_slider(self.drag_info.start_pos + delta) - - def move_slider(self, new_pos): - """Move the slider so that it's centered on new_pos.""" - if self.is_horizontal(): - size = self.allocation.width - else: - size = self.allocation.height - - slider_size = self.slider_size() - new_pos -= slider_size / 2 - size -= slider_size - ratio = max(0, min(1, float(new_pos) / size)) - self.set_value(ratio * (self.max - self.min)) - - wrappermap.wrapper(self).emit('moved', self.get_value()) - if self.is_continuous(): - wrappermap.wrapper(self).emit('changed', self.get_value()) - - def handle_drag_out_of_bounds(self): - if not self.is_continuous(): - self.set_value(self.start_value) - - def do_button_release_event(self, event): - if self.drag_info is None or event.button != self.drag_info.button: - return - self.drag_info = None - if (self.is_continuous and - (0 <= event.x < self.allocation.width) and - (0 <= event.y < self.allocation.height)): - wrappermap.wrapper(self).emit('changed', self.get_value()) - wrappermap.wrapper(self).emit('released') - - def do_scroll_event(self, event): - wrapper = wrappermap.wrapper(self) - if self.is_horizontal(): - if event.direction == gtk.gdk.SCROLL_UP: - event.direction = gtk.gdk.SCROLL_DOWN - elif event.direction == gtk.gdk.SCROLL_DOWN: - event.direction = gtk.gdk.SCROLL_UP - if (wrapper._scroll_step is not None and - event.direction in (gtk.gdk.SCROLL_UP, gtk.gdk.SCROLL_DOWN)): - # handle the scroll ourself - if event.direction == gtk.gdk.SCROLL_DOWN: - delta = wrapper._scroll_step - else: - delta = -wrapper._scroll_step - self.set_value(self.get_value() + delta) - else: - # let GTK handle the scroll - self.gtk_scale_class().do_scroll_event(self, event) - # Treat mouse scrolls as if the user clicked on the new position - wrapper.emit('pressed') - wrapper.emit('changed', self.get_value()) - wrapper.emit('released') - - def do_move_slider(self, scroll): - if self.is_horizontal(): - if scroll == gtk.SCROLL_STEP_UP: - scroll = gtk.SCROLL_STEP_DOWN - elif scroll == gtk.SCROLL_STEP_DOWN: - scroll = gtk.SCROLL_STEP_UP - elif scroll == gtk.SCROLL_PAGE_UP: - scroll = gtk.SCROLL_PAGE_DOWN - elif scroll == gtk.SCROLL_PAGE_DOWN: - scroll = gtk.SCROLL_PAGE_UP - elif scroll == gtk.SCROLL_START: - scroll = gtk.SCROLL_END - elif scroll == gtk.SCROLL_END: - scroll = gtk.SCROLL_START - return self.gtk_scale_class().do_move_slider(self, scroll) - -class CustomHScaleWidget(CustomScaleMixin, gtk.HScale): - def __init__(self): - CustomScaleMixin.__init__(self) - gtk.HScale.__init__(self) - - def is_horizontal(self): - return True - -class CustomVScaleWidget(CustomScaleMixin, gtk.VScale): - def __init__(self): - CustomScaleMixin.__init__(self) - gtk.VScale.__init__(self) - - def is_horizontal(self): - return False - -gobject.type_register(CustomButtonWidget) -gobject.type_register(ContinuousCustomButtonWidget) -gobject.type_register(DragableCustomButtonWidget) -gobject.type_register(CustomHScaleWidget) -gobject.type_register(CustomVScaleWidget) - -class CustomControlBase(Drawable, Widget): - def __init__(self): - Widget.__init__(self) - Drawable.__init__(self) - self._gtk_cursor = None - self._entry_handlers = None - - def _connect_enter_notify_handlers(self): - if self._entry_handlers is None: - self._entry_handlers = [ - self.wrapped_widget_connect('enter-notify-event', - self.on_enter_notify), - self.wrapped_widget_connect('leave-notify-event', - self.on_leave_notify), - self.wrapped_widget_connect('button-release-event', - self.on_click) - ] - - def _disconnect_enter_notify_handlers(self): - if self._entry_handlers is not None: - for handle in self._entry_handlers: - self._widget.disconnect(handle) - self._entry_handlers = None - - def set_cursor(self, cursor): - if cursor == widgetconst.CURSOR_NORMAL: - self._gtk_cursor = None - self._disconnect_enter_notify_handlers() - elif cursor == widgetconst.CURSOR_POINTING_HAND: - self._gtk_cursor = gtk.gdk.Cursor(gtk.gdk.HAND2) - self._connect_enter_notify_handlers() - else: - raise ValueError("Unknown cursor: %s" % cursor) - - def on_enter_notify(self, widget, event): - self._widget.window.set_cursor(self._gtk_cursor) - - def on_leave_notify(self, widget, event): - if self._widget.window: - self._widget.window.set_cursor(None) - - def on_click(self, widget, event): - self.emit('clicked') - return True - -class CustomButton(CustomControlBase): - def __init__(self): - """Create a new CustomButton. active_image will be displayed while - the button is pressed. The image must have the same size. - """ - CustomControlBase.__init__(self) - self.set_widget(CustomButtonWidget()) - self.create_signal('clicked') - self.forward_signal('clicked') - -class DragableCustomButton(CustomControlBase): - def __init__(self): - CustomControlBase.__init__(self) - self.set_widget(DragableCustomButtonWidget()) - self.create_signal('clicked') - self.create_signal('dragged-left') - self.create_signal('dragged-right') - -class CustomSlider(CustomControlBase): - def __init__(self): - CustomControlBase.__init__(self) - self.create_signal('pressed') - self.create_signal('released') - self.create_signal('changed') - self.create_signal('moved') - self._scroll_step = None - if self.is_horizontal(): - self.set_widget(CustomHScaleWidget()) - else: - self.set_widget(CustomVScaleWidget()) - self.wrapped_widget_connect('move-slider', self.on_slider_move) - - def on_slider_move(self, widget, scrolltype): - self.emit('changed', widget.get_value()) - self.emit('moved', widget.get_value()) - - def get_value(self): - return self._widget.get_value() - - def set_value(self, value): - self._widget.set_value(value) - - def get_range(self): - return self._widget.get_range() - - def get_slider_pos(self, value=None): - """Get the position for the slider for our current value. - - This will return position that the slider should be centered on to - display the value. It will be the x coordinate if is_horizontal() is - True and the y coordinate otherwise. - - This method takes into acount the size of the slider when calculating - the position. The slider position will start at (slider_size / 2) and - will end (slider_size / 2) px before the end of the widget. - - :param value: value to get the position for. Defaults to the current - value - """ - return self._widget.get_slider_pos(value) - - def set_range(self, min_value, max_value): - self._widget.set_range(min_value, max_value) - # set_digits controls the precision of the scale by limiting changes - # to a certain number of digits. If the range is [0, 1], this code - # will give us 4 digits of precision, which seems reasonable. - range = max_value - min_value - self._widget.set_digits(int(round(math.log10(10000.0 / range)))) - - def set_increments(self, small_step, big_step, scroll_step=None): - """Set the increments to scroll. - - :param small_step: scroll amount for up/down - :param big_step: scroll amount for page up/page down. - :param scroll_step: scroll amount for mouse wheel, or None to make - this 2 times the small step - """ - self._widget.set_increments(small_step, big_step) - self._scroll_step = scroll_step - -def to_miro_volume(value): - """Convert from 0 to 1.0 to 0.0 to MAX_VOLUME. - """ - if value == 0: - return 0.0 - return value * widgetconst.MAX_VOLUME - -def to_gtk_volume(value): - """Convert from 0.0 to MAX_VOLUME to 0 to 1.0. - """ - if value > 0.0: - value = (value / widgetconst.MAX_VOLUME) - return value - -if hasattr(gtk.VolumeButton, "get_popup"): - # FIXME - Miro on Windows has an old version of gtk (2.16) and - # doesn't have the get_popup method. Once we upgrade and - # fix that, we can take out the hasattr check. - - class VolumeMuter(Label): - """Empty space that has a clicked signal so it can be dropped - in place of the VolumeMuter. - """ - def __init__(self): - Label.__init__(self) - self.create_signal("clicked") - - class VolumeSlider(Widget): - """VolumeSlider that uses the gtk.VolumeButton(). - """ - def __init__(self): - Widget.__init__(self) - self.set_widget(gtk.VolumeButton()) - self.wrapped_widget_connect('value-changed', self.on_value_changed) - self._widget.get_popup().connect("hide", self.on_hide) - self.create_signal('changed') - self.create_signal('released') - - def on_value_changed(self, *args): - value = self.get_value() - self.emit('changed', value) - - def on_hide(self, *args): - self.emit('released') - - def get_value(self): - value = self._widget.get_property('value') - return to_miro_volume(value) - - def set_value(self, value): - value = to_gtk_volume(value) - self._widget.set_property('value', value) - -class ClickableImageButton(CustomButton): - """Image that can send clicked events. If max_width and/or max_height are - specified, resizes the image proportionally such that all constraints are - met. - """ - def __init__(self, image_path, max_width=None, max_height=None): - CustomButton.__init__(self) - self.max_width = max_width - self.max_height = max_height - self.image = None - self._width, self._height = None, None - if image_path: - self.set_path(image_path) - self.set_cursor(widgetconst.CURSOR_POINTING_HAND) - - def set_path(self, path): - image = Image(path) - if self.max_width: - image = image.resize_for_space(self.max_width, self.max_height) - self.image = ImageSurface(image) - self._width, self._height = image.width, image.height - - def size_request(self, layout): - w = self._width - h = self._height - if not w: - w = self.max_width - if not h: - h = self.max_height - return w, h - - def draw(self, context, layout): - if self.image: - self.image.draw(context, 0, 0, self._width, self._height) - w = self._width - h = self._height - if not w: - w = self.max_width - if not h: - h = self.max_height - w = min(context.width, w) - h = min(context.height, h) - context.rectangle(0, 0, w, h) - context.set_color((0, 0, 0)) # black - context.set_line_width(1) - context.stroke() diff --git a/mvc/widgets/gtk/drawing.py b/mvc/widgets/gtk/drawing.py deleted file mode 100644 index 5888851..0000000 --- a/mvc/widgets/gtk/drawing.py +++ /dev/null @@ -1,268 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -""".drawing -- Contains classes used to draw on -widgets. -""" - -import cairo -import gobject -import gtk - -import wrappermap -from .base import Widget, Bin -from .layoutmanager import LayoutManager - -def css_to_color(css_string): - parts = (css_string[1:3], css_string[3:5], css_string[5:7]) - return tuple((int(value, 16) / 255.0) for value in parts) - -class ImageSurface: - def __init__(self, image): - format = cairo.FORMAT_RGB24 - if image.pixbuf.get_has_alpha(): - format = cairo.FORMAT_ARGB32 - self.image = cairo.ImageSurface( - format, int(image.width), int(image.height)) - context = cairo.Context(self.image) - gdkcontext = gtk.gdk.CairoContext(context) - gdkcontext.set_source_pixbuf(image.pixbuf, 0, 0) - gdkcontext.paint() - self.pattern = cairo.SurfacePattern(self.image) - self.pattern.set_extend(cairo.EXTEND_REPEAT) - self.width = image.width - self.height = image.height - - def get_size(self): - return self.width, self.height - - def _align_pattern(self, x, y): - """Line up our image pattern so that it's top-left corner is x, y.""" - m = cairo.Matrix() - m.translate(-x, -y) - self.pattern.set_matrix(m) - - def draw(self, context, x, y, width, height, fraction=1.0): - self._align_pattern(x, y) - cairo_context = context.context - cairo_context.save() - cairo_context.set_source(self.pattern) - cairo_context.new_path() - cairo_context.rectangle(x, y, width, height) - if fraction >= 1.0: - cairo_context.fill() - else: - cairo_context.clip() - cairo_context.paint_with_alpha(fraction) - cairo_context.restore() - - def draw_rect(self, context, dest_x, dest_y, source_x, source_y, - width, height, fraction=1.0): - - self._align_pattern(dest_x-source_x, dest_y-source_y) - cairo_context = context.context - cairo_context.save() - cairo_context.set_source(self.pattern) - cairo_context.new_path() - cairo_context.rectangle(dest_x, dest_y, width, height) - if fraction >= 1.0: - cairo_context.fill() - else: - cairo_context.clip() - cairo_context.paint_with_alpha(fraction) - cairo_context.restore() - -class DrawingStyle(object): - def __init__(self, widget, use_base_color=False, state=None): - if state is None: - state = widget._widget.state - self.use_custom_style = widget.use_custom_style - self.style = widget._widget.style - self.text_color = widget.convert_gtk_color(self.style.text[state]) - if use_base_color: - self.bg_color = widget.convert_gtk_color(self.style.base[state]) - else: - self.bg_color = widget.convert_gtk_color(self.style.bg[state]) - -class DrawingContext(object): - """DrawingContext. This basically just wraps a Cairo context and adds a - couple convenience methods. - """ - - def __init__(self, window, drawing_area, expose_area): - self.window = window - self.context = window.cairo_create() - self.context.rectangle(expose_area.x, expose_area.y, - expose_area.width, expose_area.height) - self.context.clip() - self.width = drawing_area.width - self.height = drawing_area.height - self.context.translate(drawing_area.x, drawing_area.y) - - def __getattr__(self, name): - return getattr(self.context, name) - - def set_color(self, (red, green, blue), alpha=1.0): - self.context.set_source_rgba(red, green, blue, alpha) - - def set_shadow(self, color, opacity, offset, blur_radius): - pass - - def gradient_fill(self, gradient): - old_source = self.context.get_source() - self.context.set_source(gradient.pattern) - self.context.fill() - self.context.set_source(old_source) - - def gradient_fill_preserve(self, gradient): - old_source = self.context.get_source() - self.context.set_source(gradient.pattern) - self.context.fill_preserve() - self.context.set_source(old_source) - -class Gradient(object): - def __init__(self, x1, y1, x2, y2): - self.pattern = cairo.LinearGradient(x1, y1, x2, y2) - - def set_start_color(self, (red, green, blue)): - self.pattern.add_color_stop_rgb(0, red, green, blue) - - def set_end_color(self, (red, green, blue)): - self.pattern.add_color_stop_rgb(1, red, green, blue) - -class CustomDrawingMixin(object): - def do_expose_event(self, event): - wrapper = wrappermap.wrapper(self) - if self.flags() & gtk.NO_WINDOW: - drawing_area = self.allocation - else: - drawing_area = gtk.gdk.Rectangle(0, 0, - self.allocation.width, self.allocation.height) - context = DrawingContext(event.window, drawing_area, event.area) - context.style = DrawingStyle(wrapper) - if self.flags() & gtk.CAN_FOCUS: - focus_space = (self.style_get_property('focus-padding') + - self.style_get_property('focus-line-width')) - if not wrapper.squish_width: - context.width -= focus_space * 2 - translate_x = focus_space - else: - translate_x = 0 - if not wrapper.squish_height: - context.height -= focus_space * 2 - translate_y = focus_space - else: - translate_y = 0 - context.translate(translate_x, translate_y) - wrapper.layout_manager.update_cairo_context(context.context) - self.draw(wrapper, context) - - def draw(self, wrapper, context): - wrapper.layout_manager.reset() - wrapper.draw(context, wrapper.layout_manager) - - def do_size_request(self, requesition): - wrapper = wrappermap.wrapper(self) - width, height = wrapper.size_request(wrapper.layout_manager) - requesition.width = width - requesition.height = height - if self.flags() & gtk.CAN_FOCUS: - focus_space = (self.style_get_property('focus-padding') + - self.style_get_property('focus-line-width')) - if not wrapper.squish_width: - requesition.width += focus_space * 2 - if not wrapper.squish_height: - requesition.height += focus_space * 2 - -class MiroDrawingArea(CustomDrawingMixin, gtk.Widget): - def __init__(self): - gtk.Widget.__init__(self) - CustomDrawingMixin.__init__(self) - self.set_flags(gtk.NO_WINDOW) - -class BackgroundWidget(CustomDrawingMixin, gtk.Bin): - def do_size_request(self, requesition): - CustomDrawingMixin.do_size_request(self, requesition) - if self.get_child(): - child_width, child_height = self.get_child().size_request() - requesition.width = max(child_width, requesition.width) - requesition.height = max(child_height, requesition.height) - - def do_expose_event(self, event): - CustomDrawingMixin.do_expose_event(self, event) - if self.get_child(): - self.propagate_expose(self.get_child(), event) - - def do_size_allocate(self, allocation): - gtk.Bin.do_size_allocate(self, allocation) - if self.get_child(): - self.get_child().size_allocate(allocation) - -gobject.type_register(MiroDrawingArea) -gobject.type_register(BackgroundWidget) - -class Drawable: - def __init__(self): - self.squish_width = self.squish_height = False - - def set_squish_width(self, setting): - self.squish_width = setting - - def set_squish_height(self, setting): - self.squish_height = setting - - def set_widget(self, drawing_widget): - if self.is_opaque() and 0: - box = gtk.EventBox() - box.add(drawing_widget) - Widget.set_widget(self, box) - else: - Widget.set_widget(self, drawing_widget) - self.layout_manager = LayoutManager(self._widget) - - def size_request(self, layout_manager): - return 0, 0 - - def draw(self, context, layout_manager): - pass - - def is_opaque(self): - return False - -class DrawingArea(Drawable, Widget): - def __init__(self): - Widget.__init__(self) - Drawable.__init__(self) - self.set_widget(MiroDrawingArea()) - -class Background(Drawable, Bin): - def __init__(self): - Bin.__init__(self) - Drawable.__init__(self) - self.set_widget(BackgroundWidget()) diff --git a/mvc/widgets/gtk/gtkmenus.py b/mvc/widgets/gtk/gtkmenus.py deleted file mode 100644 index 926ba15..0000000 --- a/mvc/widgets/gtk/gtkmenus.py +++ /dev/null @@ -1,404 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -"""gtkmenus.py -- Manage menu layout.""" - -import gtk - -from mvc.widgets import app - -import base -import keymap -import wrappermap - -def _setup_accel(widget, name, shortcut=None): - """Setup accelerators for a menu item. - - This method sets an accel path for the widget and optionally connects a - shortcut to that accel path. - """ - # The GTK docs say that we should set the path using this form: - # <Window-Name>/Menu/Submenu/MenuItem - # ...but this is hard to do because we don't yet know what window/menu - # this menu item is going to be added to. gtk.Action and gtk.ActionGroup - # don't follow the above suggestion, so we don't need to either. - path = "<MiroActions>/MenuBar/%s" % name - widget.set_accel_path(path) - if shortcut is not None: - accel_string = keymap.get_accel_string(shortcut) - key, mods = gtk.accelerator_parse(accel_string) - if gtk.accel_map_lookup_entry(path) is None: - gtk.accel_map_add_entry(path, key, mods) - else: - gtk.accel_map_change_entry(path, key, mods, True) - -# map menu names to GTK stock ids. -_STOCK_IDS = { - "SaveItem": gtk.STOCK_SAVE, - "CopyItemURL": gtk.STOCK_COPY, - "RemoveItems": gtk.STOCK_REMOVE, - "StopItem": gtk.STOCK_MEDIA_STOP, - "NextItem": gtk.STOCK_MEDIA_NEXT, - "PreviousItem": gtk.STOCK_MEDIA_PREVIOUS, - "PlayPauseItem": gtk.STOCK_MEDIA_PLAY, - "Open": gtk.STOCK_OPEN, - "EditPreferences": gtk.STOCK_PREFERENCES, - "Quit": gtk.STOCK_QUIT, - "Help": gtk.STOCK_HELP, - "About": gtk.STOCK_ABOUT, - "Translate": gtk.STOCK_EDIT -} -try: - _STOCK_IDS['Fullscreen'] = gtk.STOCK_FULLSCREEN -except AttributeError: - # fullscreen not available on all GTK versions - pass - -class MenuItemBase(base.Widget): - """Base class for MenuItem and Separator.""" - - def show(self): - """Show this menu item.""" - self._widget.show() - - def hide(self): - """Hide and disable this menu item.""" - self._widget.hide() - - def remove_from_parent(self): - """Remove this menu item from it's parent Menu.""" - parent_menu = self._widget.get_parent() - if parent_menu is None: - return - parent_menu_item = parent_menu.get_attach_widget() - if parent_menu_item is None: - return - parent_menu_item.remove(self._widget) - - def _set_accel_group(self, accel_group): - # menu items don't care about the accel group, their parent Menu - # handles it for them - pass - -class MenuItem(MenuItemBase): - """Single item in the menu that can be clicked - - :param label: The label it has (must be internationalized) - :param name: String identifier for this item - :param shortcut: Shortcut object to use - - Signals: - - activate: menu item was clicked - - Example: - - >>> MenuItem(_("Preferences"), "EditPreferences") - >>> MenuItem(_("Cu_t"), "ClipboardCut", Shortcut("x", MOD)) - >>> MenuItem(_("_Update Podcasts and Library"), "UpdatePodcasts", - ... (Shortcut("r", MOD), Shortcut(F5))) - >>> MenuItem(_("_Play"), "PlayPauseItem", - ... play=_("_Play"), pause=_("_Pause")) - """ - - def __init__(self, label, name, shortcut=None): - MenuItemBase.__init__(self) - self.name = name - self.set_widget(self.make_widget(label)) - self.activate_id = self.wrapped_widget_connect('activate', - self._on_activate) - self._widget.show() - self.create_signal('activate') - _setup_accel(self._widget, self.name, shortcut) - - def _on_activate(self, menu_item): - self.emit('activate') - gtk_menubar = self._find_menubar() - if gtk_menubar is not None: - try: - menubar = wrappermap.wrapper(gtk_menubar) - except KeyError: - logging.exception('menubar activate: ' - 'no wrapper for gtbbk.MenuBar') - else: - menubar.emit('activate', self.name) - - def _find_menubar(self): - """Find the MenuBar that this menu item is attached to.""" - menu_item = self._widget - while True: - parent_menu = menu_item.get_parent() - if isinstance(parent_menu, gtk.MenuBar): - return parent_menu - elif parent_menu is None: - return None - menu_item = parent_menu.get_attach_widget() - if menu_item is None: - return None - - def make_widget(self, label): - """Create the menu item to use for this widget. - - Subclasses will probably want to override this. - """ - if self.name in _STOCK_IDS: - mi = gtk.ImageMenuItem(stock_id=_STOCK_IDS[self.name]) - mi.set_label(label) - return mi - else: - return gtk.MenuItem(label) - - def set_label(self, new_label): - self._widget.set_label(new_label) - - def get_label(self): - self._widget.get_label() - -class CheckMenuItem(MenuItem): - """MenuItem that toggles on/off""" - - def make_widget(self, label): - return gtk.CheckMenuItem(label) - - def set_state(self, active): - # prevent the activate signal from fireing in response to us manually - # changing a value - self._widget.handler_block(self.activate_id) - if active is not None: - self._widget.set_inconsistent(False) - self._widget.set_active(active) - else: - self._widget.set_inconsistent(True) - self._widget.set_active(False) - self._widget.handler_unblock(self.activate_id) - - def get_state(self): - return self._widget.get_active() - -class RadioMenuItem(CheckMenuItem): - """MenuItem that toggles on/off and is grouped with other RadioMenuItems. - """ - - def make_widget(self, label): - widget = gtk.RadioMenuItem() - widget.set_label(label) - return widget - - def set_group(self, group_item): - self._widget.set_group(group_item._widget) - - def remove_from_group(self): - """Remove this RadioMenuItem from its current group.""" - self._widget.set_group(None) - - def _on_activate(self, menu_item): - # GTK sends the activate signal for both the radio button that's - # toggled on and the one that gets turned off. Just emit our signal - # for the active radio button. - if self.get_state(): - MenuItem._on_activate(self, menu_item) - -class Separator(MenuItemBase): - """Separator item for menus""" - - def __init__(self): - MenuItemBase.__init__(self) - self.set_widget(gtk.SeparatorMenuItem()) - self._widget.show() - # Set name to be None just so that it has a similar API to other menu - # items. - self.name = None - -class MenuShell(base.Widget): - """Common code shared between Menu and MenuBar. - - Subclasses must define a _menu attribute that's a gtk.MenuShell subclass. - """ - - def __init__(self): - base.Widget.__init__(self) - self._accel_group = None - self.children = [] - - def append(self, menu_item): - """Add a menu item to the end of this menu.""" - self.children.append(menu_item) - menu_item._set_accel_group(self._accel_group) - self._menu.append(menu_item._widget) - - def insert(self, index, menu_item): - """Insert a menu item in the middle of this menu.""" - self.children.insert(index, menu_item) - menu_item._set_accel_group(self._accel_group) - self._menu.insert(menu_item._widget, index) - - def remove(self, menu_item): - """Remove a child menu item. - - :raises ValueError: menu_item is not a child of this menu - """ - self.children.remove(menu_item) - self._menu.remove(menu_item._widget) - menu_item._set_accel_group(None) - - def index(self, name): - """Get the position of a menu item in this list. - - :param name: name of the menu - :returns: index of the menu item, or -1 if not found. - """ - for i, menu_item in enumerate(self.children): - if menu_item.name == name: - return i - return -1 - - def get_children(self): - """Get the child menu items in order.""" - return list(self.children) - - def find(self, name): - """Search for a menu or menu item - - This method recursively searches the entire menu structure for a Menu - or MenuItem object with a given name. - - :raises KeyError: name not found - """ - found = self._find(name) - if found is None: - raise KeyError(name) - else: - return found - - def _find(self, name): - """Low-level helper-method for find(). - - :returns: found menu item or None. - """ - for menu_item in self.get_children(): - if menu_item.name == name: - return menu_item - if isinstance(menu_item, MenuShell): - submenu_find = menu_item._find(name) - if submenu_find is not None: - return submenu_find - return None - -class Menu(MenuShell): - """A Menu holds a list of MenuItems and Menus. - - Example: - >>> Menu(_("P_layback"), "Playback", [ - ... MenuItem(_("_Foo"), "Foo"), - ... MenuItem(_("_Bar"), "Bar") - ... ]) - >>> Menu("", "toplevel", [ - ... Menu(_("_File"), "File", [ ... ]) - ... ]) - """ - - def __init__(self, label, name, child_items): - MenuShell.__init__(self) - self.set_widget(gtk.MenuItem(label)) - self._widget.show() - self.name = name - # set up _menu for the MenuShell code - self._menu = gtk.Menu() - _setup_accel(self._menu, self.name) - self._widget.set_submenu(self._menu) - for item in child_items: - self.append(item) - - def show(self): - """Show this menu.""" - self._widget.show() - - def hide(self): - """Hide this menu.""" - self._widget.hide() - - def _set_accel_group(self, accel_group): - """Set the accel group for this widget. - - Accel groups get created by the MenuBar. Whenever a menu or menu item - is added to that menu bar, the parent calls _set_accel_group() to give - the accel group to the child. - """ - if accel_group == self._accel_group: - return - self._menu.set_accel_group(accel_group) - self._accel_group = accel_group - for child in self.children: - child._set_accel_group(accel_group) - -class MenuBar(MenuShell): - """Displays a list of Menu items. - - Signals: - - - activate(menu_bar, name): a menu item was activated - """ - - def __init__(self): - """Create a new MenuBar - - :param name: string id to use for our action group - """ - MenuShell.__init__(self) - self.create_signal('activate') - self.set_widget(gtk.MenuBar()) - self._widget.show() - self._accel_group = gtk.AccelGroup() - # set up _menu for the MenuShell code - self._menu = self._widget - - def get_accel_group(self): - return self._accel_group - -class MainWindowMenuBar(MenuBar): - """MenuBar for the main window. - - This gets installed into app.widgetapp.menubar on GTK. - """ - def add_initial_menus(self, menus): - """Add the initial set of menus. - - We modify the menu structure slightly for GTK. - """ - for menu in menus: - self.append(menu) - self._modify_initial_menus() - - def _modify_initial_menus(self): - """Update the portable root menu with GTK-specific stuff.""" - # on linux, we don't have a CheckVersion option because - # we update with the package system. - #this_platform = app.config.get(prefs.APP_PLATFORM) - #if this_platform == 'linux': - # self.find("CheckVersion").remove_from_parent() - #app.video_renderer.setup_subtitle_encoding_menu() diff --git a/mvc/widgets/gtk/keymap.py b/mvc/widgets/gtk/keymap.py deleted file mode 100644 index cf341ff..0000000 --- a/mvc/widgets/gtk/keymap.py +++ /dev/null @@ -1,94 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -"""keymap.py -- Map portable key values to GTK ones. -""" - -import gtk - -from mvc.widgets import keyboard - -menubar_mod_map = { - keyboard.MOD: '<Ctrl>', - keyboard.CTRL: '<Ctrl>', - keyboard.ALT: '<Alt>', - keyboard.SHIFT: '<Shift>', -} - -menubar_key_map = { - keyboard.RIGHT_ARROW: 'Right', - keyboard.LEFT_ARROW: 'Left', - keyboard.UP_ARROW: 'Up', - keyboard.DOWN_ARROW: 'Down', - keyboard.SPACE: 'space', - keyboard.ENTER: 'Return', - keyboard.DELETE: 'Delete', - keyboard.BKSPACE: 'BackSpace', - keyboard.ESCAPE: 'Escape', - '>': 'greater', - '<': 'less' -} -for i in range(1, 13): - name = 'F%d' % i - menubar_key_map[getattr(keyboard, name)] = name - -# These are reversed versions of menubar_key_map and menubar_mod_map -gtk_key_map = dict((i[1], i[0]) for i in menubar_key_map.items()) - -def get_accel_string(shortcut): - mod_str = ''.join(menubar_mod_map[mod] for mod in shortcut.modifiers) - key_str = menubar_key_map.get(shortcut.shortcut, shortcut.shortcut) - return mod_str + key_str - -def translate_gtk_modifiers(event): - """Convert a keypress event to a set of modifiers from the shortcut - module. - """ - modifiers = set() - if event.state & gtk.gdk.CONTROL_MASK: - modifiers.add(keyboard.CTRL) - if event.state & gtk.gdk.MOD1_MASK: - modifiers.add(keyboard.ALT) - if event.state & gtk.gdk.SHIFT_MASK: - modifiers.add(keyboard.SHIFT) - return modifiers - -def translate_gtk_event(event): - """Convert a GTK key event into the tuple (key, modifiers) where - key and modifiers are from the shortcut module. - """ - gtk_keyval = gtk.gdk.keyval_name(event.keyval) - if gtk_keyval == None: - return None - if len(gtk_keyval) == 1: - key = gtk_keyval - else: - key = gtk_key_map.get(gtk_keyval) - modifiers = translate_gtk_modifiers(event) - return key, modifiers diff --git a/mvc/widgets/gtk/layout.py b/mvc/widgets/gtk/layout.py deleted file mode 100644 index d887fcb..0000000 --- a/mvc/widgets/gtk/layout.py +++ /dev/null @@ -1,227 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -""".layout -- Layout widgets. """ - -import gtk - -from mvc.utils import Matrix -from .base import Widget, Bin - -class Box(Widget): - def __init__(self, spacing=0): - Widget.__init__(self) - self.children = set() - self.set_widget(self.WIDGET_CLASS(spacing=spacing)) - - def pack_start(self, widget, expand=False, padding=0): - self._widget.pack_start(widget._widget, expand, fill=True, - padding=padding) - widget._widget.show() - self.children.add(widget) - - def pack_end(self, widget, expand=False, padding=0): - self._widget.pack_end(widget._widget, expand, fill=True, - padding=padding) - widget._widget.show() - self.children.add(widget) - - def remove(self, widget): - widget._widget.hide() # otherwise gtkmozembed gets confused - self._widget.remove(widget._widget) - self.children.remove(widget) - - def enable(self): - for mem in self.children: - mem.enable() - - def disable(self): - for mem in self.children: - mem.disable() - -class HBox(Box): - WIDGET_CLASS = gtk.HBox - -class VBox(Box): - WIDGET_CLASS = gtk.VBox - -class Alignment(Bin): - def __init__(self, xalign=0, yalign=0, xscale=0, yscale=0, - top_pad=0, bottom_pad=0, left_pad=0, right_pad=0): - Bin.__init__(self) - self.set_widget(gtk.Alignment(xalign, yalign, xscale, yscale)) - self.set_padding(top_pad, bottom_pad, left_pad, right_pad) - - def set(self, xalign=0, yalign=0, xscale=0, yscale=0): - self._widget.set(xalign, yalign, xscale, yscale) - - def set_padding(self, top_pad=0, bottom_pad=0, left_pad=0, right_pad=0): - self._widget.set_padding(top_pad, bottom_pad, left_pad, right_pad) - -class DetachedWindowHolder(Alignment): - def __init__(self): - Alignment.__init__(self, xscale=1, yscale=1) - -class Splitter(Widget): - def __init__(self): - """Create a new splitter.""" - Widget.__init__(self) - self.set_widget(gtk.HPaned()) - - def set_left(self, widget): - """Set the left child widget.""" - self.left = widget - self._widget.pack1(widget._widget, resize=False, shrink=False) - widget._widget.show() - - def set_right(self, widget): - """Set the right child widget. """ - self.right = widget - self._widget.pack2(widget._widget, resize=True, shrink=False) - widget._widget.show() - - def remove_left(self): - """Remove the left child widget.""" - if self.left is not None: - self.left._widget.hide() # otherwise gtkmozembed gets confused - self._widget.remove(self.left._widget) - self.left = None - - def remove_right(self): - """Remove the right child widget.""" - if self.right is not None: - self.right._widget.hide() # otherwise gtkmozembed gets confused - self._widget.remove(self.right._widget) - self.right = None - - def set_left_width(self, width): - self._widget.set_position(width) - - def get_left_width(self): - return self._widget.get_position() - - def set_right_width(self, width): - self._widget.set_position(self.width - width) - # We should take into account the width of the bar, but this seems - # good enough. - -class Table(Widget): - """Lays out widgets in a table. It works very similar to the GTK Table - widget, or an HTML table. - """ - def __init__(self, columns, rows): - Widget.__init__(self) - self.set_widget(gtk.Table(rows, columns, homogeneous=False)) - self.children = Matrix(columns, rows) - - def pack(self, widget, column, row, column_span=1, row_span=1): - """Add a widget to the table. - """ - self.children[column, row] = widget - self._widget.attach(widget._widget, column, column + column_span, - row, row + row_span) - widget._widget.show() - - def remove(self, widget): - widget._widget.hide() # otherwise gtkmozembed gets confused - self.children.remove(widget) - self._widget.remove(widget._widget) - - def set_column_spacing(self, spacing): - self._widget.set_col_spacings(spacing) - - def set_row_spacing(self, spacing): - self._widget.set_row_spacings(spacing) - - def enable(self, row=None, column=None): - if row != None and column != None: - if self.children[column, row]: - self.children[column, row].enable() - elif row != None: - for mem in self.children.row(row): - if mem: mem.enable() - elif column != None: - for mem in self.children.column(column): - if mem: mem.enable() - else: - for mem in self.children: - if mem: mem.enable() - - def disable(self, row=None, column=None): - if row != None and column != None: - if self.children[column, row]: - self.children[column, row].disable() - elif row != None: - for mem in self.children.row(row): - if mem: mem.disable() - elif column != None: - for mem in self.children.column(column): - if mem: mem.disable() - else: - for mem in self.children: - if mem: mem.disable() - -class TabContainer(Widget): - def __init__(self, xalign=0, yalign=0, xscale=0, yscale=0, - top_pad=0, bottom_pad=0, left_pad=0, right_pad=0): - Widget.__init__(self) - self.set_widget(gtk.Notebook()) - self._widget.set_tab_pos(gtk.POS_TOP) - self.children = [] - self._page_to_select = None - self.wrapped_widget_connect('realize', self._on_realize) - - def _on_realize(self, widget): - if self._page_to_select is not None: - self._widget.set_current_page(self._page_to_select) - self._page_to_select = None - - def append_tab(self, child_widget, text, image=None): - if image is not None: - label_widget = gtk.VBox(spacing=2) - image_widget = gtk.Image() - image_widget.set_from_pixbuf(image.pixbuf) - label_widget.pack_start(image_widget) - label_widget.pack_start(gtk.Label(text)) - label_widget.show_all() - else: - label_widget = gtk.Label(text) - - # switch from a center align to a top align - child_widget.set(0, 0, 1, 0) - child_widget.set_padding(10, 10, 10, 10) - - self._widget.append_page(child_widget._widget, label_widget) - self.children.append(child_widget) - - def select_tab(self, index): - if self._widget.flags() & gtk.REALIZED: - self._widget.set_current_page(index) - else: - self._page_to_select = index diff --git a/mvc/widgets/gtk/layoutmanager.py b/mvc/widgets/gtk/layoutmanager.py deleted file mode 100644 index fb60049..0000000 --- a/mvc/widgets/gtk/layoutmanager.py +++ /dev/null @@ -1,550 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -"""drawing.py -- Contains the LayoutManager class. LayoutManager is -handles laying out complex objects for the custom drawing code like -text blocks and buttons. -""" - -import math - -import cairo -import gtk -import pango - -from mvc import utils - -use_native_buttons = False # not implemented in MVC - -class FontCache(utils.Cache): - def get(self, context, description, scale_factor, bold, italic): - key = (context, description, scale_factor, bold, italic) - return utils.Cache.get(self, key) - - def create_new_value(self, key, invalidator=None): - (context, description, scale_factor, bold, italic) = key - return Font(context, description, scale_factor, bold, italic) - -_font_cache = FontCache(512) - -class LayoutManager(object): - def __init__(self, widget): - self.pango_context = widget.get_pango_context() - self.update_style(widget.style) - self.update_direction(widget.get_direction()) - widget.connect('style-set', self.on_style_set) - widget.connect('direction-changed', self.on_direction_changed) - self.widget = widget - self.reset() - - def reset(self): - self.current_font = self.font(1.0) - self.text_color = (0, 0, 0) - self.text_shadow = None - - def on_style_set(self, widget, previous_style): - old_font_desc = self.style_font_desc - self.update_style(widget.style) - if self.style_font_desc != old_font_desc: - # bug #17423 font changed, so the widget's width might have changed - widget.queue_resize() - - def on_direction_changed(self, widget, previous_direction): - self.update_direction(widget.get_direction()) - - def update_style(self, style): - self.style_font_desc = style.font_desc - self.style = style - - def update_direction(self, direction): - if direction == gtk.TEXT_DIR_RTL: - self.pango_context.set_base_dir(pango.DIRECTION_RTL) - else: - self.pango_context.set_base_dir(pango.DIRECTION_LTR) - - def font(self, scale_factor, bold=False, italic=False, family=None): - return _font_cache.get(self.pango_context, self.style_font_desc, - scale_factor, bold, italic) - - def set_font(self, scale_factor, bold=False, italic=False, family=None): - self.current_font = self.font(scale_factor, bold, italic) - - def set_text_color(self, color): - self.text_color = color - - def set_text_shadow(self, shadow): - self.text_shadow = shadow - - def textbox(self, text, underline=False): - textbox = TextBox(self.pango_context, self.current_font, - self.text_color, self.text_shadow) - textbox.set_text(text, underline=underline) - return textbox - - def button(self, text, pressed=False, disabled=False, style='normal'): - if style == 'webby': - return StyledButton(text, self.pango_context, self.current_font, - pressed, disabled) - elif use_native_buttons: - return NativeButton(text, self.pango_context, self.current_font, - pressed, self.style, self.widget) - else: - return StyledButton(text, self.pango_context, self.current_font, - pressed) - - def update_cairo_context(self, cairo_context): - cairo_context.update_context(self.pango_context) - -class Font(object): - def __init__(self, context, style_font_desc, scale, bold, italic): - self.context = context - self.description = style_font_desc.copy() - self.description.set_size(int(scale * style_font_desc.get_size())) - if bold: - self.description.set_weight(pango.WEIGHT_BOLD) - if italic: - self.description.set_style(pango.STYLE_ITALIC) - self.font_metrics = None - - def get_font_metrics(self): - if self.font_metrics is None: - self.font_metrics = self.context.get_metrics(self.description) - return self.font_metrics - - def ascent(self): - return pango.PIXELS(self.get_font_metrics().get_ascent()) - - def descent(self): - return pango.PIXELS(self.get_font_metrics().get_descent()) - - def line_height(self): - metrics = self.get_font_metrics() - # the +1: some glyphs can be slightly taller than ascent+descent - # (#17329) - return (pango.PIXELS(metrics.get_ascent()) + - pango.PIXELS(metrics.get_descent()) + 1) - -class TextBox(object): - def __init__(self, context, font, color, shadow): - self.layout = pango.Layout(context) - self.layout.set_wrap(pango.WRAP_WORD_CHAR) - self.font = font - self.color = color - self.layout.set_font_description(font.description.copy()) - self.width = self.height = None - self.shadow = shadow - - def set_text(self, text, font=None, color=None, underline=False): - self.text_chunks = [] - self.attributes = [] - self.text_length = 0 - self.underlines = [] - self.append_text(text, font, color, underline) - - def append_text(self, text, font=None, color=None, underline=False): - if text == None: - text = u"" - startpos = self.text_length - self.text_chunks.append(text) - endpos = self.text_length = self.text_length + len(text) - if font is not None: - attr = pango.AttrFontDesc(font.description, startpos, endpos) - self.attributes.append(attr) - if underline: - self.underlines.append((startpos, endpos)) - if color: - def convert(value): - return int(round(value * 65535)) - attr = pango.AttrForeground(convert(color[0]), convert(color[1]), - convert(color[2]), startpos, endpos) - self.attributes.append(attr) - self.text_set = False - - def set_width(self, width): - if width is not None: - self.layout.set_width(int(width * pango.SCALE)) - else: - self.layout.set_width(-1) - self.width = width - - def set_height(self, height): - # if height is not None: - # # not sure why set_height isn't in the python bindings, but it - # # isn't - # pygtkhacks.set_pango_layout_height(self.layout, - # int(height * pango.SCALE)) - self.height = height - - def set_wrap_style(self, wrap): - if wrap == 'word': - self.layout.set_wrap(pango.WRAP_WORD_CHAR) - elif wrap == 'char' or wrap == 'truncated-char': - self.layout.set_wrap(pango.WRAP_CHAR) - else: - raise ValueError("Unknown wrap value: %s" % wrap) - if wrap == 'truncated-char': - self.layout.set_ellipsize(pango.ELLIPSIZE_END) - else: - self.layout.set_ellipsize(pango.ELLIPSIZE_NONE) - - def set_alignment(self, align): - if align == 'left': - self.layout.set_alignment(pango.ALIGN_LEFT) - elif align == 'right': - self.layout.set_alignment(pango.ALIGN_RIGHT) - elif align == 'center': - self.layout.set_alignment(pango.ALIGN_CENTER) - else: - raise ValueError("Unknown align value: %s" % align) - - def ensure_layout(self): - if not self.text_set: - text = ''.join(self.text_chunks) - if len(text) > 100: - text = text[:self._calc_text_cutoff()] - self.layout.set_text(text) - attr_list = pango.AttrList() - for attr in self.attributes: - attr_list.insert(attr) - self.layout.set_attributes(attr_list) - self.text_set = True - - def _calc_text_cutoff(self): - """This method is a bit of a hack... GTK slows down if we pass too - much text to the layout. Even text that falls below our height has a - performance penalty. Try not to have too much more than is necessary. - """ - if None in (self.width, self.height): - return -1 - - chars_per_line = (self.width * pango.SCALE // - self.font.get_font_metrics().get_approximate_char_width()) - lines_available = self.height // self.font.line_height() - # overestimate these because it's better to have too many characters - # than too little. - return int(chars_per_line * lines_available * 1.2) - - def line_count(self): - self.ensure_layout() - return self.layout.get_line_count() - - def get_size(self): - self.ensure_layout() - return self.layout.get_pixel_size() - - def char_at(self, x, y): - self.ensure_layout() - x *= pango.SCALE - y *= pango.SCALE - width, height = self.layout.get_size() - if 0 <= x < width and 0 <= y < height: - index, leading = self.layout.xy_to_index(x, y) - # xy_to_index returns the nearest character, but that - # doesn't mean the user actually clicked on it. Double - # check that (x, y) is actually inside that char's - # bounding box - char_x, char_y, char_w, char_h = self.layout.index_to_pos(index) - if char_w > 0: # the glyph is LTR - left = char_x - right = char_x + char_w - else: # the glyph is RTL - left = char_x + char_w - right = char_x - if left <= x < right: - return index - return None - - - def draw(self, context, x, y, width, height): - self.set_width(width) - self.set_height(height) - self.ensure_layout() - cairo_context = context.context - cairo_context.save() - underline_drawer = UnderlineDrawer(self.underlines) - if self.shadow: - # draw shadow first so that it's underneath the regular text - # FIXME: we don't use the blur_radius setting - cairo_context.set_source_rgba(self.shadow.color[0], - self.shadow.color[1], self.shadow.color[2], - self.shadow.opacity) - self._draw_layout(context, x + self.shadow.offset[0], - y + self.shadow.offset[1], width, height, - underline_drawer) - cairo_context.set_source_rgb(*self.color) - self._draw_layout(context, x, y, width, height, underline_drawer) - cairo_context.restore() - cairo_context.new_path() - - def _draw_layout(self, context, x, y, width, height, underline_drawer): - line_height = 0 - alignment = self.layout.get_alignment() - for i in xrange(self.layout.get_line_count()): - line = self.layout.get_line_readonly(i) - extents = line.get_pixel_extents()[1] - next_line_height = line_height + extents[3] - if next_line_height > height: - break - if alignment == pango.ALIGN_CENTER: - line_x = max(x, x + (width - extents[2]) / 2.0) - elif alignment == pango.ALIGN_RIGHT: - line_x = max(x, x + width - extents[2]) - else: - line_x = x - baseline = y + line_height + pango.ASCENT(extents) - context.move_to(line_x, baseline) - context.context.show_layout_line(line) - underline_drawer.draw(context, line_x, baseline, line) - line_height = next_line_height - -class UnderlineDrawer(object): - """Class to draw our own underlines because cairo's don't look - that great at small fonts. We make sure that the underline is - always drawn at a pixel boundary and that there always is space - between the text and the baseline. - - This class makes a couple assumptions that might not be that - great. It assumes that the correct underline size is 1 pixel and - that the text color doesn't change in the middle of an underline. - """ - def __init__(self, underlines): - self.underline_iter = iter(underlines) - self.finished = False - self.next_underline() - - def next_underline(self): - try: - self.startpos, self.endpos = self.underline_iter.next() - except StopIteration: - self.finished = True - else: - # endpos is the char to stop underlining at - self.endpos -= 1 - - def draw(self, context, x, baseline, line): - baseline = round(baseline) + 0.5 - context.set_line_width(1) - while not self.finished and line.start_index <= self.startpos: - startpos = max(line.start_index, self.startpos) - endpos = min(self.endpos, line.start_index + line.length) - x1 = x + pango.PIXELS(line.index_to_x(startpos, 0)) - x2 = x + pango.PIXELS(line.index_to_x(endpos, 1)) - context.move_to(x1, baseline + 1) - context.line_to(x2, baseline + 1) - context.stroke() - if endpos < self.endpos: - break - else: - self.next_underline() - -class NativeButton(object): - ICON_PAD = 4 - - def __init__(self, text, context, font, pressed, style, widget): - self.layout = pango.Layout(context) - self.font = font - self.pressed = pressed - self.layout.set_font_description(font.description.copy()) - self.layout.set_text(text) - self.pad_x = style.xthickness + 11 - self.pad_y = style.ythickness + 1 - self.style = style - self.widget = widget - # The above code assumes an "inner-border" style property of - # 1. PyGTK doesn't seem to support Border objects very well, - # so can't get it from the widget style. - self.min_width = 0 - self.icon = None - - def set_min_width(self, width): - self.min_width = width - - def set_icon(self, icon): - self.icon = icon - - def get_size(self): - width, height = self.layout.get_pixel_size() - if self.icon: - width += self.icon.width + self.ICON_PAD - height = max(height, self.icon.height) - width += self.pad_x * 2 - height += self.pad_y * 2 - return max(self.min_width, width), height - - def draw(self, context, x, y, width, height): - text_width, text_height = self.layout.get_pixel_size() - if self.icon: - inner_width = text_width + self.icon.width + self.ICON_PAD - # calculate the icon position x and y are still in cairo - # coordinates - icon_x = x + (width - inner_width) / 2.0 - icon_y = y + (height - self.icon.height) / 2.0 - text_x = icon_x + self.icon.width + self.ICON_PAD - else: - text_x = x + (width - text_width) / 2.0 - text_y = y + (height - text_height) / 2.0 - - x, y = context.context.user_to_device(x, y) - text_x, text_y = context.context.user_to_device(text_x, text_y) - # Hmm, maybe we should somehow support floating point numbers - # here, but I don't know how to. - x, y, width, height = (int(f) for f in (x, y, width, height)) - context.context.get_target().flush() - self.draw_box(context.window, x, y, width, height) - self.draw_text(context.window, text_x, text_y) - if self.icon: - self.icon.draw(context, icon_x, icon_y, self.icon.width, - self.icon.height) - - def draw_box(self, window, x, y, width, height): - if self.pressed: - shadow = gtk.SHADOW_IN - state = gtk.STATE_ACTIVE - else: - shadow = gtk.SHADOW_OUT - state = gtk.STATE_NORMAL - if 'QtCurveStyle' in str(self.style): - # This is a horrible hack for the libqtcurve library. See - # http://bugzilla.pculture.org/show_bug.cgi?id=10380 for - # details - widget = window.get_user_data() - else: - widget = self.widget - - self.style.paint_box(window, state, shadow, None, widget, "button", - int(x), int(y), int(width), int(height)) - - def draw_text(self, window, x, y): - if self.pressed: - state = gtk.STATE_ACTIVE - else: - state = gtk.STATE_NORMAL - self.style.paint_layout(window, state, True, None, None, None, - int(x), int(y), self.layout) - -class StyledButton(object): - PAD_HORIZONTAL = 4 - PAD_VERTICAL = 3 - TOP_COLOR = (1, 1, 1) - BOTTOM_COLOR = (0.86, 0.86, 0.86) - LINE_COLOR_TOP = (0.71, 0.71, 0.71) - LINE_COLOR_BOTTOM = (0.45, 0.45, 0.45) - TEXT_COLOR = (0.184, 0.184, 0.184) - DISABLED_COLOR = (0.86, 0.86, 0.86) - DISABLED_TEXT_COLOR = (0.5, 0.5, 0.5) - ICON_PAD = 8 - - def __init__(self, text, context, font, pressed, disabled=False): - self.layout = pango.Layout(context) - self.font = font - self.layout.set_font_description(font.description.copy()) - self.layout.set_text(text) - self.min_width = 0 - self.pressed = pressed - self.disabled = disabled - self.icon = None - - def set_icon(self, icon): - self.icon = icon - - def set_min_width(self, width): - self.min_width = width - - def get_size(self): - width, height = self.layout.get_pixel_size() - if self.icon: - width += self.icon.width + self.ICON_PAD - height = max(height, self.icon.height) - height += self.PAD_VERTICAL * 2 - if height % 2 == 1: - # make height even so that the radius of our circle is - # whole - height += 1 - width += self.PAD_HORIZONTAL * 2 + height - return max(self.min_width, width), height - - def draw_path(self, context, x, y, width, height, radius): - inner_width = width - radius * 2 - context.move_to(x + radius, y) - context.rel_line_to(inner_width, 0) - context.arc(x + width - radius, y+radius, radius, -math.pi/2, - math.pi/2) - context.rel_line_to(-inner_width, 0) - context.arc(x + radius, y+radius, radius, math.pi/2, -math.pi/2) - - def draw_button(self, context, x, y, width, height, radius): - context.context.save() - self.draw_path(context, x, y, width, height, radius) - if self.disabled: - end_color = self.DISABLED_COLOR - start_color = self.DISABLED_COLOR - elif self.pressed: - end_color = self.TOP_COLOR - start_color = self.BOTTOM_COLOR - else: - context.set_line_width(1) - start_color = self.TOP_COLOR - end_color = self.BOTTOM_COLOR - gradient = cairo.LinearGradient(x, y, x, y + height) - gradient.add_color_stop_rgb(0, *start_color) - gradient.add_color_stop_rgb(1, *end_color) - context.context.set_source(gradient) - context.fill() - context.set_line_width(1) - self.draw_path(context, x+0.5, y+0.5, width, height, radius) - gradient = cairo.LinearGradient(x, y, x, y + height) - gradient.add_color_stop_rgb(0, *self.LINE_COLOR_TOP) - gradient.add_color_stop_rgb(1, *self.LINE_COLOR_BOTTOM) - context.context.set_source(gradient) - context.stroke() - context.context.restore() - - def draw(self, context, x, y, width, height): - radius = height / 2 - self.draw_button(context, x, y, width, height, radius) - - text_width, text_height = self.layout.get_pixel_size() - # draw the text in the center of the button - text_x = x + (width - text_width) / 2 - text_y = y + (height - text_height) / 2 - if self.icon: - icon_x = text_x - (self.icon.width + self.ICON_PAD) / 2 - text_x += (self.icon.width + self.ICON_PAD) / 2 - icon_y = y + (height - self.icon.height) / 2 - self.icon.draw(context, icon_x, icon_y, self.icon.width, - self.icon.height) - self.draw_text(context, text_x, text_y, width, height, radius) - - def draw_text(self, context, x, y, width, height, radius): - if self.disabled: - context.set_color(self.DISABLED_TEXT_COLOR) - else: - context.set_color(self.TEXT_COLOR) - context.move_to(x, y) - context.context.show_layout(self.layout) diff --git a/mvc/widgets/gtk/simple.py b/mvc/widgets/gtk/simple.py deleted file mode 100644 index f0921e0..0000000 --- a/mvc/widgets/gtk/simple.py +++ /dev/null @@ -1,313 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -"""simple.py -- Collection of simple widgets.""" - -import gtk -import gobject -import pango - -from mvc.widgets import widgetconst -from .base import Widget, Bin - -class Image(object): - def __init__(self, path): - try: - self._set_pixbuf(gtk.gdk.pixbuf_new_from_file(path)) - except gobject.GError, ge: - raise ValueError("%s" % ge) - self.width = self.pixbuf.get_width() - self.height = self.pixbuf.get_height() - - def _set_pixbuf(self, pixbuf): - self.pixbuf = pixbuf - self.width = self.pixbuf.get_width() - self.height = self.pixbuf.get_height() - - def resize(self, width, height): - width = int(round(width)) - height = int(round(height)) - resized_pixbuf = self.pixbuf.scale_simple(width, height, - gtk.gdk.INTERP_BILINEAR) - return TransformedImage(resized_pixbuf) - - def resize_for_space(self, width, height): - """Returns an image scaled to fit into the specified space at the - correct height/width ratio. - """ - ratio = min(1.0 * width / self.width, 1.0 * height / self.height) - return self.resize(ratio * self.width, ratio * self.height) - - def crop_and_scale(self, src_x, src_y, src_width, src_height, dest_width, - dest_height): - """Crop an image then scale it. - - The image will be cropped to the rectangle (src_x, src_y, src_width, - src_height), that rectangle will be scaled to a new Image with tisez - (dest_width, dest_height) - """ - dest = gtk.gdk.Pixbuf(self.pixbuf.get_colorspace(), - self.pixbuf.get_has_alpha(), - self.pixbuf.get_bits_per_sample(), dest_width, dest_height) - - scale_x = dest_width / float(src_width) - scale_y = dest_height / float(src_height) - - self.pixbuf.scale(dest, 0, 0, dest_width, dest_height, - -src_x * scale_x, -src_y * scale_y, scale_x, scale_y, - gtk.gdk.INTERP_BILINEAR) - return TransformedImage(dest) - -class TransformedImage(Image): - def __init__(self, pixbuf): - # XXX intentionally not calling direct super's __init__; we should do - # this differently - self._set_pixbuf(pixbuf) - -class ImageDisplay(Widget): - def __init__(self, image=None): - Widget.__init__(self) - self.set_widget(gtk.Image()) - self.set_image(image) - - def set_image(self, image): - self.image = image - if image is not None: - self._widget.set_from_pixbuf(image.pixbuf) - else: - self._widget.clear() - -class AnimatedImageDisplay(Widget): - def __init__(self, path): - Widget.__init__(self) - self.set_widget(gtk.Image()) - self._animation = gtk.gdk.PixbufAnimation(path) - # Set to animate before we are shown and stop animating after - # we disappear. - self._widget.connect('map', lambda w: self._set_animate(True)) - self._widget.connect('unmap-event', - lambda w, a: self._set_animate(False)) - - def _set_animate(self, enabled): - if enabled: - self._widget.set_from_animation(self._animation) - else: - self._widget.clear() - -class Label(Widget): - """Widget that displays simple text.""" - def __init__(self, text="", color=None): - Widget.__init__(self) - self.set_widget(gtk.Label()) - if text: - self.set_text(text) - self.attr_list = pango.AttrList() - self.font_description = self._widget.style.font_desc.copy() - self.scale_factor = 1.0 - if color is not None: - self.set_color(color) - self.wrapped_widget_connect('style-set', self.on_style_set) - - def set_bold(self, bold): - if bold: - weight = pango.WEIGHT_BOLD - else: - weight = pango.WEIGHT_NORMAL - self.font_description.set_weight(weight) - self.set_attr(pango.AttrFontDesc(self.font_description)) - - def set_size(self, size): - if size == widgetconst.SIZE_NORMAL: - self.scale_factor = 1 - elif size == widgetconst.SIZE_SMALL: - self.scale_factor = 0.75 - else: - self.scale_factor = size - baseline = self._widget.style.font_desc.get_size() - self.font_description.set_size(int(baseline * self.scale_factor)) - self.set_attr(pango.AttrFontDesc(self.font_description)) - - def get_preferred_width(self): - return self._widget.size_request()[0] - - def on_style_set(self, widget, old_style): - self.set_size(self.scale_factor) - - def set_wrap(self, wrap): - self._widget.set_line_wrap(wrap) - - def set_alignment(self, alignment): - # default to left. - gtkalignment = gtk.JUSTIFY_LEFT - if alignment == widgetconst.TEXT_JUSTIFY_LEFT: - gtkalignment = gtk.JUSTIFY_LEFT - elif alignment == widgetconst.TEXT_JUSTIFY_RIGHT: - gtkalignment = gtk.JUSTIFY_RIGHT - elif alignment == widgetconst.TEXT_JUSTIFY_CENTER: - gtkalignment = gtk.JUSTIFY_CENTER - self._widget.set_justify(gtkalignment) - - def get_alignment(self): - return self._widget.get_justify() - - def get_width(self): - return self._widget.get_layout().get_pixel_size()[0] - - def set_text(self, text): - self._widget.set_text(text) - - def get_text(self): - return self._widget.get_text().decode('utf-8') - - def set_selectable(self, val): - self._widget.set_selectable(val) - - def set_attr(self, attr): - attr.end_index = 65535 - self.attr_list.change(attr) - self._widget.set_attributes(self.attr_list) - - def set_color(self, color): - color_as_int = (int(65535 * c) for c in color) - self.set_attr(pango.AttrForeground(*color_as_int)) - - def baseline(self): - pango_context = self._widget.get_pango_context() - metrics = pango_context.get_metrics(self.font_description) - return pango.PIXELS(metrics.get_descent()) - - def hide(self): - self._widget.hide() - - def show(self): - self._widget.show() - -class Scroller(Bin): - def __init__(self, horizontal, vertical): - Bin.__init__(self) - self.set_widget(gtk.ScrolledWindow()) - if horizontal: - h_policy = gtk.POLICY_AUTOMATIC - else: - h_policy = gtk.POLICY_NEVER - if vertical: - v_policy = gtk.POLICY_AUTOMATIC - else: - v_policy = gtk.POLICY_NEVER - self._widget.set_policy(h_policy, v_policy) - - def set_has_borders(self, has_border): - pass - - def set_background_color(self, color): - pass - - def add_child_to_widget(self): - if (isinstance(self.child._widget, gtk.TreeView) or - isinstance(self.child._widget, gtk.TextView)): - # child has native scroller - self._widget.add(self.child._widget) - else: - self._widget.add_with_viewport(self.child._widget) - self._widget.get_child().set_shadow_type(gtk.SHADOW_NONE) - if isinstance(self.child._widget, gtk.TextView): - self._widget.set_shadow_type(gtk.SHADOW_IN) - else: - self._widget.set_shadow_type(gtk.SHADOW_NONE) - - def prepare_for_dark_content(self): - # this is just a hack for cocoa - pass - - -class SolidBackground(Bin): - def __init__(self, color=None): - Bin.__init__(self) - self.set_widget(gtk.EventBox()) - if color is not None: - self.set_background_color(color) - - def set_background_color(self, color): - self.modify_style('base', gtk.STATE_NORMAL, self.make_color(color)) - self.modify_style('bg', gtk.STATE_NORMAL, self.make_color(color)) - -class Expander(Bin): - def __init__(self, child=None): - Bin.__init__(self) - self.set_widget(gtk.Expander()) - if child is not None: - self.add(child) - self.label = None - # This is a complete hack. GTK expanders have a transparent - # background most of the time, except when they are prelighted. So we - # just set the background to white there because that's what should - # happen in the item list. - self.modify_style('bg', gtk.STATE_PRELIGHT, - gtk.gdk.color_parse('white')) - - def set_spacing(self, spacing): - self._widget.set_spacing(spacing) - - def set_label(self, widget): - self.label = widget - self._widget.set_label_widget(widget._widget) - widget._widget.show() - - def set_expanded(self, expanded): - self._widget.set_expanded(expanded) - -class ProgressBar(Widget): - def __init__(self): - Widget.__init__(self) - self.set_widget(gtk.ProgressBar()) - self._timer = None - - def set_progress(self, fraction): - self._widget.set_fraction(fraction) - - def start_pulsing(self): - if self._timer is None: - self._timer = gobject.timeout_add(100, self._do_pulse) - - def stop_pulsing(self): - if self._timer: - gobject.source_remove(self._timer) - self._timer = None - - def _do_pulse(self): - self._widget.pulse() - return True - -class HLine(Widget): - """A horizontal separator. Not to be confused with HSeparator, which is is - a DrawingArea, not a Widget. - """ - def __init__(self): - Widget.__init__(self) - self.set_widget(gtk.HSeparator()) diff --git a/mvc/widgets/gtk/tableview.py b/mvc/widgets/gtk/tableview.py deleted file mode 100644 index 930270c..0000000 --- a/mvc/widgets/gtk/tableview.py +++ /dev/null @@ -1,1557 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -"""tableview.py -- Wrapper for the GTKTreeView widget. It's used for the tab -list and the item list (AKA almost all of the miro). -""" - -import logging - -import itertools -import gobject -import gtk -from collections import namedtuple - -# These are probably wrong, and are placeholders for now, until custom headers -# are also implemented for GTK. -CUSTOM_HEADER_HEIGHT = 25 -HEADER_HEIGHT = 25 - -from mvc import signals -from mvc.errors import (WidgetActionError, WidgetDomainError, - WidgetRangeError, WidgetNotReadyError) -from mvc.widgets.tableselection import SelectionOwnerMixin -from mvc.widgets.tablescroll import ScrollbarOwnerMixin -import drawing -import wrappermap -from .base import Widget -from .simple import Image -from .layoutmanager import LayoutManager -from .weakconnect import weak_connect -from .tableviewcells import GTKCustomCellRenderer - - -PathInfo = namedtuple('PathInfo', 'path column x y') -Rect = namedtuple('Rect', 'x y width height') -_album_view_gtkrc_installed = False - -def _install_album_view_gtkrc(): - """Hack for styling GTKTreeView for the album view widget. - - We do a couple things: - - Remove the focus ring - - Remove any separator space. - - We do this so that we don't draw a box through the album view column for - selected rows. - """ - global _album_view_gtkrc_installed - if _album_view_gtkrc_installed: - return - rc_string = ('style "album-view-style"\n' - '{ \n' - ' GtkTreeView::vertical-separator = 0\n' - ' GtkTreeView::horizontal-separator = 0\n' - ' GtkWidget::focus-line-width = 0 \n' - '}\n' - 'widget "*.miro-album-view" style "album-view-style"\n') - gtk.rc_parse_string(rc_string) - _album_view_gtkrc_installed = True - -def rect_contains_point(rect, x, y): - return ((rect.x <= x < rect.x + rect.width) and - (rect.y <= y < rect.y + rect.height)) - -class TreeViewScrolling(object): - def __init__(self): - self.scrollbars = [] - self.scroll_positions = None, None - self.restoring_scroll = None - self.connect('parent-set', self.on_parent_set) - self.scroller = None - # hack necessary because of our weird widget hierarchy (GTK doesn't deal - # well with the Scroller's widget not being the direct parent of the - # TableView's widget.) - self._coords_working = False - - def scroll_range_changed(self): - """Faux-signal; this should all be integrated into - GTKScrollbarOwnerMixin, making this unnecessary. - """ - - @property - def manually_scrolled(self): - """Return whether the view has been scrolled explicitly by the user - since the last time it was set automatically. - """ - auto_pos = self.scroll_positions[1] - if auto_pos is None: - # if we don't have any position yet, user can't have manually - # scrolled - return False - real_pos = self.scrollbars[1].get_value() - return abs(auto_pos - real_pos) > 5 # allowing some fuzziness - - @property - def position_set(self): - """Return whether the scroll position has been set in any way.""" - return any(x is not None for x in self.scroll_positions) - - def on_parent_set(self, widget, old_parent): - """We have parent window now; we need to control its scrollbars.""" - self.set_scroller(widget.get_parent()) - - def set_scroller(self, window): - """Take control of the scrollbars of window.""" - if not isinstance(window, gtk.ScrolledWindow): - return - self.scroller = window - scrollbars = tuple(bar.get_adjustment() - for bar in (window.get_hscrollbar(), window.get_vscrollbar())) - self.scrollbars = scrollbars - for i, bar in enumerate(scrollbars): - weak_connect(bar, 'changed', self.on_scroll_range_changed, i) - if self.restoring_scroll: - self.set_scroll_position(self.restoring_scroll) - - def on_scroll_range_changed(self, adjustment, bar): - """The scrollbar might have a range now. Set its initial position if - we haven't already. - """ - self._coords_working = True - if self.restoring_scroll: - self.set_scroll_position(self.restoring_scroll) - # our wrapper handles the same thing for iters - self.scroll_range_changed() - - def set_scroll_position(self, scroll_position): - """Restore the scrollbars to a remembered state.""" - try: - self.scroll_positions = tuple(self._clip_pos(adj, x) - for adj, x in zip(self.scrollbars, scroll_position)) - except WidgetActionError, error: - logging.debug("can't scroll yet: %s", error.reason) - # try again later - self.restoring_scroll = scroll_position - else: - for adj, pos in zip(self.scrollbars, self.scroll_positions): - adj.set_value(pos) - self.restoring_scroll = None - - def _clip_pos(self, adj, pos): - lower = adj.get_lower() - upper = adj.get_upper() - adj.get_page_size() - # currently, StandardView gets an upper of 2.0 when it's not ready - # FIXME: don't count on that - if pos > upper and upper < 5: - raise WidgetRangeError("scrollable area", pos, lower, upper) - return min(max(pos, lower), upper) - - def get_path_rect(self, path): - """Return the Rect for the given item, in tree coords.""" - if not self._coords_working: - # part of solution to #17405; widget_to_tree_coords tends to return - # y=8 before the first scroll-range-changed signal. ugh. - raise WidgetNotReadyError('_coords_working') - rect = self.get_background_area(path, self.get_columns()[0]) - x, y = self.widget_to_tree_coords(rect.x, rect.y) - return Rect(x, y, rect.width, rect.height) - - @property - def _scrollbars(self): - if not self.scrollbars: - raise WidgetNotReadyError - return self.scrollbars - - def scroll_ancestor(self, newly_selected, down): - # Try to figure out what just became selected. If multiple things - # somehow became selected, select the outermost one - if len(newly_selected) == 0: - raise WidgetActionError("need at an item to scroll to") - if down: - path_to_show = max(newly_selected) - else: - path_to_show = min(newly_selected) - - if not self.scrollbars: - return - vadjustment = self.scrollbars[1] - - rect = self.get_background_area(path_to_show, self.get_columns()[0]) - _, top = self.translate_coordinates(self.scroller, 0, rect.y) - top += vadjustment.value - bottom = top + rect.height - if down: - if bottom > vadjustment.value + vadjustment.page_size: - bottom_value = min(bottom, vadjustment.upper) - vadjustment.set_value(bottom_value - vadjustment.page_size) - else: - if top < vadjustment.value: - vadjustment.set_value(max(vadjustment.lower, top)) - -class MiroTreeView(gtk.TreeView, TreeViewScrolling): - """Extends the GTK TreeView widget to help implement TableView - https://develop.participatoryculture.org/index.php/WidgetAPITableView""" - # Add a tiny bit of padding so that the user can drag feeds below - # the table, i.e. to the bottom row, as a top-level - PAD_BOTTOM = 3 - def __init__(self): - gtk.TreeView.__init__(self) - TreeViewScrolling.__init__(self) - self.height_without_pad_bottom = -1 - self.set_enable_search(False) - self.horizontal_separator = self.style_get_property("horizontal-separator") - self.expander_size = self.style_get_property("expander-size") - self.group_lines_enabled = False - self.group_line_color = (0, 0, 0) - self.group_line_width = 1 - - def do_size_request(self, req): - gtk.TreeView.do_size_request(self, req) - self.height_without_pad_bottom = req.height - req.height += self.PAD_BOTTOM - - def do_move_cursor(self, step, count): - if step == gtk.MOVEMENT_VISUAL_POSITIONS: - # GTK is asking us to move left/right. Since our TableViews don't - # support this, return False to let the key press propagate. See - # #15646 for more info. - return False - if isinstance(self.get_parent(), gtk.ScrolledWindow): - # If our parent is a ScrolledWindow, let GTK take care of this - handled = gtk.TreeView.do_move_cursor(self, step, count) - return handled - else: - # Otherwise, we have to search up the widget tree for a - # ScrolledWindow to take care of it - selection = self.get_selection() - model, start_selection = selection.get_selected_rows() - gtk.TreeView.do_move_cursor(self, step, count) - - model, end_selection = selection.get_selected_rows() - newly_selected = set(end_selection) - set(start_selection) - down = (count > 0) - - try: - self.scroll_ancestor(newly_selected, down) - except WidgetActionError: - # not possible - return False - return True - - def get_position_info(self, x, y): - """Wrapper for get_path_at_pos that converts the path_info to a named - tuple and handles rounding the coordinates. - """ - path_info = self.get_path_at_pos(int(round(x)), int(round(y))) - if path_info: - return PathInfo(*path_info) - -gobject.type_register(MiroTreeView) - -class HotspotTracker(object): - """Handles tracking hotspots. - https://develop.participatoryculture.org/index.php/WidgetAPITableView""" - - def __init__(self, treeview, event): - self.treeview = treeview - self.treeview_wrapper = wrappermap.wrapper(treeview) - self.hit = False - self.button = event.button - path_info = treeview.get_position_info(event.x, event.y) - if path_info is None: - return - self.path, self.column, background_x, background_y = path_info - # We always pack 1 renderer for each column - gtk_renderer = self.column.get_cell_renderers()[0] - if not isinstance(gtk_renderer, GTKCustomCellRenderer): - return - self.renderer = wrappermap.wrapper(gtk_renderer) - self.attr_map = self.treeview_wrapper.attr_map_for_column[self.column] - if not rect_contains_point(self.calc_cell_area(), event.x, event.y): - # Mouse is in the padding around the actual cell area - return - self.update_position(event) - self.iter = treeview.get_model().get_iter(self.path) - self.name = self.calc_hotspot() - if self.name is not None: - self.hit = True - - def is_for_context_menu(self): - return self.name == "#show-context-menu" - - def calc_cell_area(self): - cell_area = self.treeview.get_cell_area(self.path, self.column) - xpad = self.renderer._renderer.props.xpad - ypad = self.renderer._renderer.props.ypad - cell_area.x += xpad - cell_area.y += ypad - cell_area.width -= xpad * 2 - cell_area.height -= ypad * 2 - return cell_area - - def update_position(self, event): - self.x, self.y = int(event.x), int(event.y) - - def calc_cell_state(self): - if self.treeview.get_selection().path_is_selected(self.path): - if self.treeview.flags() & gtk.HAS_FOCUS: - return gtk.STATE_SELECTED - else: - return gtk.STATE_ACTIVE - else: - return gtk.STATE_NORMAL - - def calc_hotspot(self): - cell_area = self.calc_cell_area() - if rect_contains_point(cell_area, self.x, self.y): - model = self.treeview.get_model() - self.renderer.cell_data_func(self.column, self.renderer._renderer, - model, self.iter, self.attr_map) - style = drawing.DrawingStyle(self.treeview_wrapper, - use_base_color=True, state=self.calc_cell_state()) - x = self.x - cell_area.x - y = self.y - cell_area.y - return self.renderer.hotspot_test(style, - self.treeview_wrapper.layout_manager, - x, y, cell_area.width, cell_area.height) - else: - return None - - def update_hit(self): - if self.is_for_context_menu(): - return # we always keep hit = True for this one - old_hit = self.hit - self.hit = (self.calc_hotspot() == self.name) - if self.hit != old_hit: - self.redraw_cell() - - def redraw_cell(self): - # Check that the treeview is still around. We might have switched - # views in response to a hotspot being clicked. - if self.treeview.flags() & gtk.REALIZED: - cell_area = self.treeview.get_cell_area(self.path, self.column) - x, y = self.treeview.tree_to_widget_coords(cell_area.x, - cell_area.y) - self.treeview.queue_draw_area(x, y, - cell_area.width, cell_area.height) - -class TableColumn(signals.SignalEmitter): - """A single column of a TableView. - - Signals: - - clicked (table_column) -- The header for this column was clicked. - """ - # GTK hard-codes 4px of padding for each column - FIXED_PADDING = 4 - def __init__(self, title, renderer, header=None, **attrs): - # header widget not used yet in GTK (#15800) - signals.SignalEmitter.__init__(self) - self.create_signal('clicked') - self._column = gtk.TreeViewColumn(title, renderer._renderer) - self._column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) - self._column.set_clickable(True) - self.attrs = attrs - renderer.setup_attributes(self._column, attrs) - self.renderer = renderer - weak_connect(self._column, 'clicked', self._header_clicked) - self.do_horizontal_padding = True - - def set_right_aligned(self, right_aligned): - """Horizontal alignment of the header label.""" - if right_aligned: - self._column.set_alignment(1.0) - else: - self._column.set_alignment(0.0) - - def set_min_width(self, width): - self._column.props.min_width = width + TableColumn.FIXED_PADDING - - def set_max_width(self, width): - self._column.props.max_width = width - - def set_width(self, width): - self._column.set_fixed_width(width + TableColumn.FIXED_PADDING) - - def get_width(self): - return self._column.get_width() - - def _header_clicked(self, tablecolumn): - self.emit('clicked') - - def set_resizable(self, resizable): - """Set if the user can resize the column.""" - self._column.set_resizable(resizable) - - def set_do_horizontal_padding(self, horizontal_padding): - self.do_horizontal_padding = False - - def set_sort_indicator_visible(self, visible): - """Show/Hide the sort indicator for this column.""" - self._column.set_sort_indicator(visible) - - def get_sort_indicator_visible(self): - return self._column.get_sort_indicator() - - def set_sort_order(self, ascending): - """Display a sort indicator on the column header. Ascending can be - either True or False which affects the direction of the indicator. - """ - if ascending: - self._column.set_sort_order(gtk.SORT_ASCENDING) - else: - self._column.set_sort_order(gtk.SORT_DESCENDING) - - def get_sort_order_ascending(self): - """Returns if the sort indicator is displaying that the sort is - ascending. - """ - return self._column.get_sort_order() == gtk.SORT_ASCENDING - -class GTKSelectionOwnerMixin(SelectionOwnerMixin): - """GTK-specific methods for selection management. - - This subclass should not define any behavior. Methods that cannot be - completed in this widget state should raise WidgetActionError. - """ - def __init__(self): - SelectionOwnerMixin.__init__(self) - self.selection = self._widget.get_selection() - weak_connect(self.selection, 'changed', self.on_selection_changed) - - def _set_allow_multiple_select(self, allow): - if allow: - mode = gtk.SELECTION_MULTIPLE - else: - mode = gtk.SELECTION_SINGLE - self.selection.set_mode(mode) - - def _get_allow_multiple_select(self): - return self.selection.get_mode() == gtk.SELECTION_MULTIPLE - - def _get_selected_iters(self): - iters = [] - def collect(treemodel, path, iter_): - iters.append(iter_) - self.selection.selected_foreach(collect) - return iters - - def _get_selected_iter(self): - model, iter_ = self.selection.get_selected() - return iter_ - - @property - def num_rows_selected(self): - return self.selection.count_selected_rows() - - def _is_selected(self, iter_): - return self.selection.iter_is_selected(iter_) - - def _select(self, iter_): - self.selection.select_iter(iter_) - - def _unselect(self, iter_): - self.selection.unselect_iter(iter_) - - def _unselect_all(self): - self.selection.unselect_all() - - def _iter_to_string(self, iter_): - return self._model.get_string_from_iter(iter_) - - def _iter_from_string(self, string): - try: - return self._model.get_iter_from_string(string) - except ValueError: - raise WidgetDomainError( - "model iters", string, "%s other iters" % len(self.model)) - - def select_path(self, path): - self.selection.select_path(path) - - def _validate_iter(self, iter_): - if self.get_path(iter_) is None: - raise WidgetDomainError( - "model iters", iter_, "%s other iters" % len(self.model)) - real_model = self._widget.get_model() - if not real_model: - raise WidgetActionError("no model") - elif real_model != self._model: - raise WidgetActionError("wrong model?") - - def get_cursor(self): - """Return the path of the 'focused' item.""" - path, column = self._widget.get_cursor() - return path - - def set_cursor(self, path): - """Set the path of the 'focused' item.""" - if path is None: - # XXX: is there a way to clear the cursor? - return - path_as_string = ':'.join(str(component) for component in path) - with self.preserving_selection(): # set_cursor() messes up the selection - self._widget.set_cursor(path_as_string) - -class DNDHandlerMixin(object): - """TableView row DnD. - - Depends on arbitrary TableView methods; otherwise self-contained except: - on_button_press: may call start_drag - on_button_release: may unset drag_button_down - on_motion_notify: may call potential_drag_motion - """ - def __init__(self): - self.drag_button_down = False - self.drag_data = {} - self.drag_source = self.drag_dest = None - self.drag_start_x, self.drag_start_y = None, None - self.wrapped_widget_connect('drag-data-get', self.on_drag_data_get) - self.wrapped_widget_connect('drag-end', self.on_drag_end) - self.wrapped_widget_connect('drag-motion', self.on_drag_motion) - self.wrapped_widget_connect('drag-leave', self.on_drag_leave) - self.wrapped_widget_connect('drag-drop', self.on_drag_drop) - self.wrapped_widget_connect('drag-data-received', - self.on_drag_data_received) - self.wrapped_widget_connect('unrealize', self.on_drag_unrealize) - - def set_drag_source(self, drag_source): - self.drag_source = drag_source - # XXX: the following note no longer seems accurate: - # No need to call enable_model_drag_source() here, we handle it - # ourselves in on_motion_notify() - - def set_drag_dest(self, drag_dest): - """Set the drop handler.""" - self.drag_dest = drag_dest - if drag_dest is not None: - targets = self._gtk_target_list(drag_dest.allowed_types()) - self._widget.enable_model_drag_dest(targets, - drag_dest.allowed_actions()) - self._widget.drag_dest_set(0, targets, - drag_dest.allowed_actions()) - else: - self._widget.unset_rows_drag_dest() - self._widget.drag_dest_unset() - - def start_drag(self, treeview, event, path_info): - """Check whether the event is a drag event; return whether handled - here. - """ - if event.state & (gtk.gdk.CONTROL_MASK | gtk.gdk.SHIFT_MASK): - return False - model, row_paths = treeview.get_selection().get_selected_rows() - - if path_info.path not in row_paths: - # something outside the selection is being dragged. - # make it the new selection. - self.unselect_all(signal=False) - self.select_path(path_info.path) - row_paths = [path_info.path] - rows = self.model.get_rows(row_paths) - self.drag_data = rows and self.drag_source.begin_drag(self, rows) - self.drag_button_down = bool(self.drag_data) - if self.drag_button_down: - self.drag_start_x = int(event.x) - self.drag_start_y = int(event.y) - - if len(row_paths) > 1 and path_info.path in row_paths: - # handle multiple selection. If the current row is already - # selected, stop propagating the signal. We will only change - # the selection if the user doesn't start a DnD operation. - # This makes it more natural for the user to drag a block of - # selected items. - renderer = path_info.column.get_cell_renderers()[0] - if (not self._x_coord_in_expander(treeview, path_info) - and not isinstance(renderer, GTKCheckboxCellRenderer)): - self.delaying_press = True - # grab keyboard focus since we handled the event - self.focus() - return True - - def on_drag_data_get(self, treeview, context, selection, info, timestamp): - for typ, data in self.drag_data.items(): - selection.set(typ, 8, repr(data)) - - def on_drag_end(self, treeview, context): - self.drag_data = {} - - def find_type(self, drag_context): - return self._widget.drag_dest_find_target(drag_context, - self._widget.drag_dest_get_target_list()) - - def calc_positions(self, x, y): - """Given x and y coordinates, generate a list of drop positions to - try. The values are tuples in the form of (parent_path, position, - gtk_path, gtk_position), where parent_path and position is the - position to send to the Miro code, and gtk_path and gtk_position is an - equivalent position to send to the GTK code if the drag_dest validates - the drop. - """ - model = self._model - try: - gtk_path, gtk_position = self._widget.get_dest_row_at_pos(x, y) - except TypeError: - # Below the last row - yield (None, len(model), None, None) - return - - iter_ = model.get_iter(gtk_path) - if gtk_position in (gtk.TREE_VIEW_DROP_INTO_OR_BEFORE, - gtk.TREE_VIEW_DROP_INTO_OR_AFTER): - yield (iter_, -1, gtk_path, gtk_position) - - if hasattr(model, 'iter_is_valid'): - # tablist has this; item list does not - assert model.iter_is_valid(iter_) - parent_iter = model.iter_parent(iter_) - position = gtk_path[-1] - if gtk_position in (gtk.TREE_VIEW_DROP_BEFORE, - gtk.TREE_VIEW_DROP_INTO_OR_BEFORE): - # gtk gave us a "before" position, no need to change it - yield (parent_iter, position, gtk_path, gtk.TREE_VIEW_DROP_BEFORE) - else: - # gtk gave us an "after" position, translate that to before the - # next row for miro. - if (self._widget.row_expanded(gtk_path) and - model.iter_has_child(iter_)): - child_path = gtk_path + (0,) - yield (iter_, 0, child_path, gtk.TREE_VIEW_DROP_BEFORE) - else: - yield (parent_iter, position+1, gtk_path, - gtk.TREE_VIEW_DROP_AFTER) - - def on_drag_motion(self, treeview, drag_context, x, y, timestamp): - if not self.drag_dest: - return True - type = self.find_type(drag_context) - if type == "NONE": - drag_context.drag_status(0, timestamp) - return True - drop_action = 0 - for pos_info in self.calc_positions(x, y): - drop_action = self.drag_dest.validate_drop(self, self.model, type, - drag_context.actions, pos_info[0], pos_info[1]) - if isinstance(drop_action, (list, tuple)): - drop_action, iter = drop_action - path = self.model.get_path(iter) - pos = gtk.TREE_VIEW_DROP_INTO_OR_BEFORE - else: - path, pos = pos_info[2:4] - - if drop_action: - self.set_drag_dest_row(path, pos) - break - else: - self.unset_drag_dest_row() - drag_context.drag_status(drop_action, timestamp) - return True - - def set_drag_dest_row(self, path, position): - self._widget.set_drag_dest_row(path, position) - - def unset_drag_dest_row(self): - self._widget.unset_drag_dest_row() - - def on_drag_leave(self, treeview, drag_context, timestamp): - treeview.unset_drag_dest_row() - - def on_drag_drop(self, treeview, drag_context, x, y, timestamp): - # prevent the default handler - treeview.emit_stop_by_name('drag-drop') - target = self.find_type(drag_context) - if target == "NONE": - return False - treeview.drag_get_data(drag_context, target, timestamp) - treeview.unset_drag_dest_row() - - def on_drag_data_received(self, - treeview, drag_context, x, y, selection, info, timestamp): - # prevent the default handler - treeview.emit_stop_by_name('drag-data-received') - if not self.drag_dest: - return - type = self.find_type(drag_context) - if type == "NONE": - return - if selection.data is None: - return - drop_action = 0 - for pos_info in self.calc_positions(x, y): - drop_action = self.drag_dest.validate_drop(self, self.model, type, - drag_context.actions, pos_info[0], pos_info[1]) - if drop_action: - self.drag_dest.accept_drop(self, self.model, type, - drag_context.actions, pos_info[0], pos_info[1], - eval(selection.data)) - return True - return False - - def on_drag_unrealize(self, treeview): - self.drag_button_down = False - - def potential_drag_motion(self, treeview, event): - """A motion event has occurred and did not hit a hotspot; start a drag - if applicable. - """ - if (self.drag_data and self.drag_button_down and - treeview.drag_check_threshold(self.drag_start_x, - self.drag_start_y, int(event.x), int(event.y))): - self.delaying_press = False - treeview.drag_begin(self._gtk_target_list(self.drag_data.keys()), - self.drag_source.allowed_actions(), 1, event) - - @staticmethod - def _gtk_target_list(types): - count = itertools.count() - return [(type, gtk.TARGET_SAME_APP, count.next()) for type in types] - -class HotspotTrackingMixin(object): - def __init__(self): - self.hotspot_tracker = None - self.create_signal('hotspot-clicked') - self._hotspot_callback_handles = [] - self._connect_hotspot_signals() - self.wrapped_widget_connect('unrealize', self.on_hotspot_unrealize) - - def _connect_hotspot_signals(self): - SIGNALS = { - 'row-inserted': self.on_row_inserted, - 'row-deleted': self.on_row_deleted, - 'row-changed': self.on_row_changed, - } - self._hotspot_callback_handles.extend( - weak_connect(self._model, signal, handler) - for signal, handler in SIGNALS.iteritems()) - - def _disconnect_hotspot_signals(self): - for handle in self._hotspot_callback_handles: - self._model.disconnect(handle) - self._hotspot_callback_handles = [] - - def on_row_inserted(self, model, path, iter_): - if self.hotspot_tracker: - self.hotspot_tracker.redraw_cell() - self.hotspot_tracker = None - - def on_row_deleted(self, model, path): - if self.hotspot_tracker: - self.hotspot_tracker.redraw_cell() - self.hotspot_tracker = None - - def on_row_changed(self, model, path, iter_): - if self.hotspot_tracker: - self.hotspot_tracker.update_hit() - - def handle_hotspot_hit(self, treeview, event): - """Check whether the event is a hotspot event; return whether handled - here. - """ - if self.hotspot_tracker: - return - hotspot_tracker = HotspotTracker(treeview, event) - if hotspot_tracker.hit: - self.hotspot_tracker = hotspot_tracker - hotspot_tracker.redraw_cell() - if hotspot_tracker.is_for_context_menu(): - menu = self._popup_context_menu(self.hotspot_tracker.path, event) - if menu: - menu.connect('selection-done', - self._on_hotspot_context_menu_selection_done) - # grab keyboard focus since we handled the event - self.focus() - return True - - def _on_hotspot_context_menu_selection_done(self, menu): - # context menu is closed, we won't get the button-release-event in - # this case, but we can unset hotspot tracker here. - if self.hotspot_tracker: - self.hotspot_tracker.redraw_cell() - self.hotspot_tracker = None - - def on_hotspot_unrealize(self, treeview): - self.hotspot_tracker = None - - def release_on_hotspot(self, event): - """A button_release occurred; return whether it has been handled as a - hotspot hit. - """ - hotspot_tracker = self.hotspot_tracker - if hotspot_tracker and event.button == hotspot_tracker.button: - hotspot_tracker.update_position(event) - hotspot_tracker.update_hit() - if (hotspot_tracker.hit and - not hotspot_tracker.is_for_context_menu()): - self.emit('hotspot-clicked', hotspot_tracker.name, - hotspot_tracker.iter) - hotspot_tracker.redraw_cell() - self.hotspot_tracker = None - return True - - def hotspot_model_changed(self): - """A bulk change has ended; reconnect signals and update hotspots.""" - self._connect_hotspot_signals() - if self.hotspot_tracker: - self.hotspot_tracker.redraw_cell() - self.hotspot_tracker.update_hit() - -class ColumnOwnerMixin(object): - """Keeps track of the table's columns - including the list of columns, and - properties that we set for a table but need to apply to each column. - - This manages: - columns - attr_map_for_column - gtk_column_to_wrapper - for use throughout tableview. - """ - def __init__(self): - self._columns_draggable = False - self._renderer_xpad = self._renderer_ypad = 0 - self.columns = [] - self.attr_map_for_column = {} - self.gtk_column_to_wrapper = {} - self.create_signal('reallocate-columns') # not emitted on GTK - - def remove_column(self, index): - """Remove a column from the display and forget it from the column lists. - """ - column = self.columns.pop(index) - del self.attr_map_for_column[column._column] - del self.gtk_column_to_wrapper[column._column] - self._widget.remove_column(column._column) - - def get_columns(self): - """Returns the current columns, in order, by title.""" - # FIXME: this should probably return column objects, and really should - # not be keeping track of columns by title at all - titles = [column.get_title().decode('utf-8') - for column in self._widget.get_columns()] - return titles - - def add_column(self, column): - """Append a column to this table; setup all necessary mappings, and - setup the new column's properties to match the table's settings. - """ - self.model.check_new_column(column) - self._widget.append_column(column._column) - self.columns.append(column) - self.attr_map_for_column[column._column] = column.attrs - self.gtk_column_to_wrapper[column._column] = column - self.setup_new_column(column) - - def setup_new_column(self, column): - """Apply properties that we keep track of at the table level to a - newly-created column. - """ - if self.background_color: - column.renderer._renderer.set_property('cell-background-gdk', - self.background_color) - column._column.set_reorderable(self._columns_draggable) - if column.do_horizontal_padding: - column.renderer._renderer.set_property('xpad', self._renderer_xpad) - column.renderer._renderer.set_property('ypad', self._renderer_ypad) - - def set_column_spacing(self, space): - """Set the amount of space between columns.""" - self._renderer_xpad = space / 2 - for column in self.columns: - if column.do_horizontal_padding: - column.renderer._renderer.set_property('xpad', - self._renderer_xpad) - - def set_row_spacing(self, space): - """Set the amount of space between columns.""" - self._renderer_ypad = space / 2 - for column in self.columns: - column.renderer._renderer.set_property('ypad', self._renderer_ypad) - - def set_columns_draggable(self, setting): - """Set the draggability of existing and future columns.""" - self._columns_draggable = setting - for column in self.columns: - column._column.set_reorderable(setting) - - def set_column_background_color(self): - """Set the background color of existing columns to the table's - background_color. - """ - for column in self.columns: - column.renderer._renderer.set_property('cell-background-gdk', - self.background_color) - - def set_auto_resizes(self, setting): - # FIXME: to be implemented. - # At this point, GTK somehow does the right thing anyway in terms of - # auto-resizing. I'm not sure exactly what's happening, but I believe - # that if the column widths don't add up to the total width, - # gtk.TreeView allocates extra width for the last column. This works - # well enough for the tab list and item list, since there's only one - # column. - pass - -class HoverTrackingMixin(object): - """Handle mouse hover events - tooltips for some cells and hover events for - renderers which support them. - """ - def __init__(self): - self.hover_info = None - self.hover_pos = None - if hasattr(self, 'get_tooltip'): - # this should probably be something like self.set_tooltip_source - self._widget.set_property('has-tooltip', True) - self.wrapped_widget_connect('query-tooltip', self.on_tooltip) - self._last_tooltip_place = None - - def on_tooltip(self, treeview, x, y, keyboard_mode, tooltip): - # x, y are relative to the entire widget, but we want them to be - # relative to our bin window. The bin window doesn't include things - # like the column headers. - origin = treeview.window.get_origin() - bin_origin = treeview.get_bin_window().get_origin() - x += origin[0] - bin_origin[0] - y += origin[1] - bin_origin[1] - path_info = treeview.get_position_info(x, y) - if path_info is None: - self._last_tooltip_place = None - return False - if (self._last_tooltip_place is not None and - path_info[:2] != self._last_tooltip_place): - # the default GTK behavior is to keep the tooltip in the same - # position, but this is looks bad when we move to a different row. - # So return False once to stop this. - self._last_tooltip_place = None - return False - self._last_tooltip_place = path_info[:2] - iter_ = treeview.get_model().get_iter(path_info.path) - column = self.gtk_column_to_wrapper[path_info.column] - text = self.get_tooltip(iter_, column) - if text is None: - return False - pygtkhacks.set_tooltip_text(tooltip, text) - return True - - def _update_hover(self, treeview, event): - old_hover_info, old_hover_pos = self.hover_info, self.hover_pos - path_info = treeview.get_position_info(event.x, event.y) - if (path_info and - self.gtk_column_to_wrapper[path_info.column].renderer.want_hover): - self.hover_info = path_info.path, path_info.column - self.hover_pos = path_info.x, path_info.y - else: - self.hover_info = None - self.hover_pos = None - if (old_hover_info != self.hover_info or - old_hover_pos != self.hover_pos): - if (old_hover_info != self.hover_info and - old_hover_info is not None): - self._redraw_cell(treeview, *old_hover_info) - if self.hover_info is not None: - self._redraw_cell(treeview, *self.hover_info) - -class GTKScrollbarOwnerMixin(ScrollbarOwnerMixin): - # XXX this is half a wrapper for TreeViewScrolling. A lot of things will - # become much simpler when we integrate TVS into this - def __init__(self): - ScrollbarOwnerMixin.__init__(self) - # super uses this for postponed scroll_to_iter - # it's a faux-signal from our _widget; this hack is only necessary until - # we integrate TVS - self._widget.scroll_range_changed = (lambda *a: - self.emit('scroll-range-changed')) - - def set_scroller(self, scroller): - """Set the Scroller object for this widget, if its ScrolledWindow is - not a direct ancestor of the object. Standard View needs this. - """ - self._widget.set_scroller(scroller._widget) - - def _set_scroll_position(self, scroll_pos): - self._widget.set_scroll_position(scroll_pos) - - def _get_item_area(self, iter_): - return self._widget.get_path_rect(self.get_path(iter_)) - - @property - def _manually_scrolled(self): - return self._widget.manually_scrolled - - @property - def _position_set(self): - return self._widget.position_set - - def _get_visible_area(self): - """Return the Rect of the visible area, in tree coords. - - get_visible_rect gets this wrong for StandardView, always returning an - origin of (0, 0) - this is because our ScrolledWindow is not our direct - parent. - """ - bars = self._widget._scrollbars - x, y = (int(adj.get_value()) for adj in bars) - width, height = (int(adj.get_page_size()) for adj in bars) - if height == 0: - # this happens even after _widget._coords_working - raise WidgetNotReadyError('visible height') - return Rect(x, y, width, height) - - def _get_scroll_position(self): - """Get the current position of both scrollbars, to restore later.""" - try: - return tuple(int(bar.get_value()) for bar in self._widget._scrollbars) - except WidgetNotReadyError: - return None - -class TableView(Widget, GTKSelectionOwnerMixin, DNDHandlerMixin, - HotspotTrackingMixin, ColumnOwnerMixin, HoverTrackingMixin, - GTKScrollbarOwnerMixin): - """https://develop.participatoryculture.org/index.php/WidgetAPITableView""" - - draws_selection = True - - def __init__(self, model, custom_headers=False): - Widget.__init__(self) - self.set_widget(MiroTreeView()) - self.model = model - self.model.add_to_tableview(self._widget) - self._model = self._widget.get_model() - wrappermap.add(self._model, model) - self._setup_colors() - self.background_color = None - self.context_menu_callback = None - self.in_bulk_change = False - self.delaying_press = False - self._use_custom_headers = False - self.layout_manager = LayoutManager(self._widget) - self.height_changed = None # 17178 hack - self._connect_signals() - # setting up mixins after general TableView init - GTKSelectionOwnerMixin.__init__(self) - DNDHandlerMixin.__init__(self) - HotspotTrackingMixin.__init__(self) - ColumnOwnerMixin.__init__(self) - HoverTrackingMixin.__init__(self) - GTKScrollbarOwnerMixin.__init__(self) - if custom_headers: - self._enable_custom_headers() - - # FIXME: should implement set_model() and make None a special case. - def unset_model(self): - """Disconnect our model from this table view. - - This should be called when you want to destroy a TableView and - there's a new TableView sharing its model. - """ - self._widget.set_model(None) - self.model = None - - def _connect_signals(self): - self.create_signal('row-expanded') - self.create_signal('row-collapsed') - self.create_signal('row-clicked') - self.create_signal('row-activated') - self.wrapped_widget_connect('row-activated', self.on_row_activated) - self.wrapped_widget_connect('row-expanded', self.on_row_expanded) - self.wrapped_widget_connect('row-collapsed', self.on_row_collapsed) - self.wrapped_widget_connect('button-press-event', self.on_button_press) - self.wrapped_widget_connect('button-release-event', - self.on_button_release) - self.wrapped_widget_connect('motion-notify-event', - self.on_motion_notify) - - def set_gradient_highlight(self, gradient): - # This is just an OS X thing. - pass - - def set_background_color(self, color): - self.background_color = self.make_color(color) - self.modify_style('base', gtk.STATE_NORMAL, self.background_color) - if not self.draws_selection: - self.modify_style('base', gtk.STATE_SELECTED, - self.background_color) - self.modify_style('base', gtk.STATE_ACTIVE, self.background_color) - if self.use_custom_style: - self.set_column_background_color() - - def set_group_lines_enabled(self, enabled): - """Enable/Disable group lines. - - This only has an effect if our model is an InfoListModel and it has a - grouping set. - - If group lines are enabled, we will draw a line below the last item in - the group. Use set_group_line_style() to change the look of the line. - """ - self._widget.group_lines_enabled = enabled - self.queue_redraw() - - def set_group_line_style(self, color, width): - self._widget.group_line_color = color - self._widget.group_line_width = width - self.queue_redraw() - - def handle_custom_style_change(self): - if self.background_color is not None: - if self.use_custom_style: - self.set_column_background_color() - else: - for column in self.columns: - column.renderer._renderer.set_property( - 'cell-background-set', False) - - def set_alternate_row_backgrounds(self, setting): - self._widget.set_rules_hint(setting) - - def set_grid_lines(self, horizontal, vertical): - if horizontal and vertical: - setting = gtk.TREE_VIEW_GRID_LINES_BOTH - elif horizontal: - setting = gtk.TREE_VIEW_GRID_LINES_HORIZONTAL - elif vertical: - setting = gtk.TREE_VIEW_GRID_LINES_VERTICAL - else: - setting = gtk.TREE_VIEW_GRID_LINES_NONE - self._widget.set_grid_lines(setting) - - def width_for_columns(self, total_width): - """Given the width allocated for the TableView, return how much of that - is available to column contents. Note that this depends on the number of - columns. - """ - column_spacing = TableColumn.FIXED_PADDING * len(self.columns) - return total_width - column_spacing - - def enable_album_view_focus_hack(self): - _install_album_view_gtkrc() - self._widget.set_name("miro-album-view") - - def focus(self): - self._widget.grab_focus() - - def _enable_custom_headers(self): - # NB: this is currently not used because the GTK tableview does not - # support custom headers. - self._use_custom_headers = True - - def set_show_headers(self, show): - self._widget.set_headers_visible(show) - self._widget.set_headers_clickable(show) - - def _setup_colors(self): - style = self._widget.style - if not self.draws_selection: - # if we don't want to draw selection, make the selected/active - # colors the same as the normal ones - self.modify_style('base', gtk.STATE_SELECTED, - style.base[gtk.STATE_NORMAL]) - self.modify_style('base', gtk.STATE_ACTIVE, - style.base[gtk.STATE_NORMAL]) - - def set_search_column(self, model_index): - self._widget.set_search_column(model_index) - - def set_fixed_height(self, fixed_height): - self._widget.set_fixed_height_mode(fixed_height) - - def set_row_expanded(self, iter_, expanded): - """Expand or collapse the row specified by iter_. Succeeds or raises - WidgetActionError. Causes row-expanded or row-collapsed to be emitted - when successful. - """ - path = self.get_path(iter_) - if expanded: - self._widget.expand_row(path, False) - else: - self._widget.collapse_row(path) - if bool(self._widget.row_expanded(path)) != bool(expanded): - raise WidgetActionError("cannot expand the given item - it " - "probably has no children.") - - def is_row_expanded(self, iter_): - path = self.get_path(iter_) - return self._widget.row_expanded(path) - - def set_context_menu_callback(self, callback): - self.context_menu_callback = callback - - # GTK is really good and it is safe to operate on table even when - # cells may be constantly changing in flux. - def set_volatile(self, volatile): - return - - def on_row_expanded(self, _widget, iter_, path): - self.emit('row-expanded', iter_, path) - - def on_row_collapsed(self, _widget, iter_, path): - self.emit('row-collapsed', iter_, path) - - def on_button_press(self, treeview, event): - """Handle a mouse button press""" - if event.type == gtk.gdk._2BUTTON_PRESS: - # already handled as row-activated - return False - - path_info = treeview.get_position_info(event.x, event.y) - if not path_info: - # no item was clicked, so it's not going to be a hotspot, drag, or - # context menu - return False - if event.type == gtk.gdk.BUTTON_PRESS: - # single click; emit the event but keep on running so we can handle - # stuff like drag and drop. - if not self._x_coord_in_expander(treeview, path_info): - iter_ = treeview.get_model().get_iter(path_info.path) - self.emit('row-clicked', iter_) - - if (event.button == 1 and self.handle_hotspot_hit(treeview, event)): - return True - if event.window != treeview.get_bin_window(): - # click is outside the content area, don't try to handle this. - # In particular, our DnD code messes up resizing table columns. - return False - if (event.button == 1 and self.drag_source and - not self._x_coord_in_expander(treeview, path_info)): - return self.start_drag(treeview, event, path_info) - elif event.button == 3 and self.context_menu_callback: - self.show_context_menu(treeview, event, path_info) - return True - - # FALLTHROUGH - return False - - def show_context_menu(self, treeview, event, path_info): - """Pop up a context menu for the given click event (which is a - right-click on a row). - """ - # hack for album view - if (treeview.group_lines_enabled and - path_info.column == treeview.get_columns()[0]): - self._select_all_rows_in_group(treeview, path_info.path) - self._popup_context_menu(path_info.path, event) - # grab keyboard focus since we handled the event - self.focus() - - def _select_all_rows_in_group(self, treeview, path): - """Select all items in the group """ - - # FIXME: this is very tightly coupled with the portable code. - - infolist = self.model - gtk_model = treeview.get_model() - if (not isinstance(infolist, InfoListModel) or - infolist.get_grouping() is None): - return - it = gtk_model.get_iter(path) - info, attrs, group_info = infolist.row_for_iter(it) - start_row = path[0] - group_info[0] - total_rows = group_info[1] - - with self._ignoring_changes(): - self.unselect_all() - for row in xrange(start_row, start_row + total_rows): - self.select_path((row,)) - self.emit('selection-changed') - - def _popup_context_menu(self, path, event): - if not self.selection.path_is_selected(path): - self.unselect_all(signal=False) - self.select_path(path) - menu = self.make_context_menu() - if menu: - menu.popup(None, None, None, event.button, event.time) - return menu - else: - return None - - # XXX treeview.get_cell_area handles what we're trying to use this for - def _x_coord_in_expander(self, treeview, path_info): - """Calculate if an x coordinate is over the expander triangle - - :param treeview: Gtk.TreeView - :param path_info: PathInfo( - tree path for the cell, - Gtk.TreeColumn, - x coordinate relative to column's cell area, - y coordinate relative to column's cell area (ignored), - ) - """ - if path_info.column != treeview.get_expander_column(): - return False - model = treeview.get_model() - if not model.iter_has_child(model.get_iter(path_info.path)): - return False - # GTK allocateds an extra 4px to the right of the expanders. This - # seems to be hardcoded as EXPANDER_EXTRA_PADDING in the source code. - total_exander_size = treeview.expander_size + 4 - # include horizontal_separator - # XXX: should this value be included in total_exander_size ? - offset = treeview.horizontal_separator / 2 - # allocate space for expanders for parent nodes - expander_start = total_exander_size * (len(path_info.path) - 1) + offset - expander_end = expander_start + total_exander_size + offset - return expander_start <= path_info.x < expander_end - - def on_row_activated(self, treeview, path, view_column): - iter_ = treeview.get_model().get_iter(path) - self.emit('row-activated', iter_) - - def make_context_menu(self): - def gen_menu(menu_items): - menu = gtk.Menu() - for menu_item_info in menu_items: - if menu_item_info is None: - item = gtk.SeparatorMenuItem() - else: - label, callback = menu_item_info - - if isinstance(label, tuple) and len(label) == 2: - text_label, icon_path = label - pixbuf = gtk.gdk.pixbuf_new_from_file(icon_path) - image = gtk.Image() - image.set_from_pixbuf(pixbuf) - item = gtk.ImageMenuItem(text_label) - item.set_image(image) - else: - item = gtk.MenuItem(label) - - if callback is None: - item.set_sensitive(False) - elif isinstance(callback, list): - item.set_submenu(gen_menu(callback)) - else: - item.connect('activate', self.on_context_menu_activate, - callback) - menu.append(item) - item.show() - return menu - - items = self.context_menu_callback(self) - if items: - return gen_menu(items) - else: - return None - - def on_context_menu_activate(self, item, callback): - callback() - - def on_button_release(self, treeview, event): - if self.release_on_hotspot(event): - return True - if event.button == 1: - self.drag_button_down = False - - if self.delaying_press: - # if dragging did not happen, unselect other rows and - # select current row - path_info = treeview.get_position_info(event.x, event.y) - if path_info is not None: - self.unselect_all(signal=False) - self.select_path(path_info.path) - self.delaying_press = False - - def _redraw_cell(self, treeview, path, column): - cell_area = treeview.get_cell_area(path, column) - x, y = treeview.convert_bin_window_to_widget_coords(cell_area.x, - cell_area.y) - treeview.queue_draw_area(x, y, cell_area.width, cell_area.height) - - def on_motion_notify(self, treeview, event): - self._update_hover(treeview, event) - - if self.hotspot_tracker: - self.hotspot_tracker.update_position(event) - self.hotspot_tracker.update_hit() - return True - - self.potential_drag_motion(treeview, event) - return None # XXX: used to fall through; not sure what retval does here - - def start_bulk_change(self): - self._widget.freeze_child_notify() - self._widget.set_model(None) - self._disconnect_hotspot_signals() - self.in_bulk_change = True - - def model_changed(self): - if self.in_bulk_change: - self._widget.set_model(self._model) - self._widget.thaw_child_notify() - self.hotspot_model_changed() - self.in_bulk_change = False - - def get_path(self, iter_): - """Always use this rather than the model's get_path directly - - if the iter isn't valid, a GTK assertion causes us to exit - without warning; this wrapper changes that to a much more useful - AssertionError. Example related bug: #17362. - """ - assert self.model.iter_is_valid(iter_) - return self._model.get_path(iter_) - -class TableModel(object): - """https://develop.participatoryculture.org/index.php/WidgetAPITableView""" - MODEL_CLASS = gtk.ListStore - - def __init__(self, *column_types): - self._model = self.MODEL_CLASS(*self.map_types(column_types)) - self._column_types = column_types - if 'image' in self._column_types: - self.convert_row_for_gtk = self.convert_row_for_gtk_slow - self.convert_value_for_gtk = self.convert_value_for_gtk_slow - else: - self.convert_row_for_gtk = self.convert_row_for_gtk_fast - self.convert_value_for_gtk = self.convert_value_for_gtk_fast - - def add_to_tableview(self, widget): - widget.set_model(self._model) - - def map_types(self, miro_column_types): - type_map = { - 'boolean': bool, - 'numeric': float, - 'integer': int, - 'text': str, - 'image': gtk.gdk.Pixbuf, - 'datetime': object, - 'object': object, - } - try: - return [type_map[type] for type in miro_column_types] - except KeyError, e: - raise ValueError("Unknown column type: %s" % e[0]) - - # If we store image data, we need to do some work to convert row data to - # send to GTK - def convert_value_for_gtk_slow(self, column_value): - if isinstance(column_value, Image): - return column_value.pixbuf - else: - return column_value - - def convert_row_for_gtk_slow(self, column_values): - return tuple(self.convert_value_for_gtk(c) for c in column_values) - - def check_new_column(self, column): - for value in column.attrs.values(): - if not isinstance(value, int): - msg = "Attribute values must be integers, not %r" % value - raise TypeError(msg) - if value < 0 or value >= len(self._column_types): - raise ValueError("Attribute index out of range: %s" % value) - - # If we don't store image data, we can don't need to do any work to - # convert row data to gtk - def convert_value_for_gtk_fast(self, value): - return value - - def convert_row_for_gtk_fast(self, column_values): - return column_values - - def append(self, *column_values): - return self._model.append(self.convert_row_for_gtk(column_values)) - - def update_value(self, iter_, index, value): - assert self._model.iter_is_valid(iter_) - self._model.set(iter_, index, self.convert_value_for_gtk(value)) - - def update(self, iter_, *column_values): - self._model[iter_] = self.convert_value_for_gtk(column_values) - - def remove(self, iter_): - if self._model.remove(iter_): - return iter_ - else: - return None - - def insert_before(self, iter_, *column_values): - row = self.convert_row_for_gtk(column_values) - return self._model.insert_before(iter_, row) - - def first_iter(self): - return self._model.get_iter_first() - - def next_iter(self, iter_): - return self._model.iter_next(iter_) - - def nth_iter(self, index): - assert index >= 0 - return self._model.iter_nth_child(None, index) - - def __iter__(self): - return iter(self._model) - - def __len__(self): - return len(self._model) - - def __getitem__(self, iter_): - return self._model[iter_] - - def get_rows(self, row_paths): - return [self._model[path] for path in row_paths] - - def get_path(self, iter_): - return self._model.get_path(iter_) - - def iter_is_valid(self, iter_): - return self._model.iter_is_valid(iter_) - -class TreeTableModel(TableModel): - """https://develop.participatoryculture.org/index.php/WidgetAPITableView""" - MODEL_CLASS = gtk.TreeStore - - def append(self, *column_values): - return self._model.append(None, self.convert_row_for_gtk( - column_values)) - - def insert_before(self, iter_, *column_values): - parent = self.parent_iter(iter_) - row = self.convert_row_for_gtk(column_values) - return self._model.insert_before(parent, iter_, row) - - def append_child(self, iter_, *column_values): - return self._model.append(iter_, self.convert_row_for_gtk( - column_values)) - - def child_iter(self, iter_): - return self._model.iter_children(iter_) - - def nth_child_iter(self, iter_, index): - assert index >= 0 - return self._model.iter_nth_child(iter_, index) - - def has_child(self, iter_): - return self._model.iter_has_child(iter_) - - def children_count(self, iter_): - return self._model.iter_n_children(iter_) - - def parent_iter(self, iter_): - assert self._model.iter_is_valid(iter_) - return self._model.iter_parent(iter_) diff --git a/mvc/widgets/gtk/tableviewcells.py b/mvc/widgets/gtk/tableviewcells.py deleted file mode 100644 index 33ac6f8..0000000 --- a/mvc/widgets/gtk/tableviewcells.py +++ /dev/null @@ -1,249 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -"""tableviewcells.py - Cell renderers for TableView.""" - -import gobject -import gtk -import pango - -from mvc import signals -from mvc.widgets import widgetconst -import drawing -import wrappermap -from .base import make_gdk_color - -class CellRenderer(object): - """Simple Cell Renderer - https://develop.participatoryculture.org/index.php/WidgetAPITableView""" - def __init__(self): - self._renderer = gtk.CellRendererText() - self.want_hover = False - - def setup_attributes(self, column, attr_map): - column.add_attribute(self._renderer, 'text', attr_map['value']) - - def set_align(self, align): - if align == 'left': - self._renderer.props.xalign = 0.0 - elif align == 'center': - self._renderer.props.xalign = 0.5 - elif align == 'right': - self._renderer.props.xalign = 1.0 - else: - raise ValueError("unknown alignment: %s" % align) - - def set_color(self, color): - self._renderer.props.foreground_gdk = make_gdk_color(color) - - def set_bold(self, bold): - font_desc = self._renderer.props.font_desc - if bold: - font_desc.set_weight(pango.WEIGHT_BOLD) - else: - font_desc.set_weight(pango.WEIGHT_NORMAL) - self._renderer.props.font_desc = font_desc - - def set_text_size(self, size): - if size == widgetconst.SIZE_NORMAL: - self._renderer.props.scale = 1.0 - elif size == widgetconst.SIZE_SMALL: - # FIXME: on 3.5 we just ignored the call. Always setting scale to - # 1.0 basically replicates that behavior, but should we actually - # try to implement the semantics of SIZE_SMALL? - self._renderer.props.scale = 1.0 - else: - raise ValueError("unknown size: %s" % size) - - def set_font_scale(self, scale_factor): - self._renderer.props.scale = scale_factor - -class ImageCellRenderer(object): - """Cell Renderer for images - https://develop.participatoryculture.org/index.php/WidgetAPITableView""" - def __init__(self): - self._renderer = gtk.CellRendererPixbuf() - self.want_hover = False - - def setup_attributes(self, column, attr_map): - column.add_attribute(self._renderer, 'pixbuf', attr_map['image']) - -class GTKCheckboxCellRenderer(gtk.CellRendererToggle): - def do_activate(self, event, treeview, path, background_area, cell_area, - flags): - iter = treeview.get_model().get_iter(path) - self.set_active(not self.get_active()) - wrappermap.wrapper(self).emit('clicked', iter) - -gobject.type_register(GTKCheckboxCellRenderer) - -class CheckboxCellRenderer(signals.SignalEmitter): - """Cell Renderer for booleans - https://develop.participatoryculture.org/index.php/WidgetAPITableView""" - def __init__(self): - signals.SignalEmitter.__init__(self) - self.create_signal("clicked") - self._renderer = GTKCheckboxCellRenderer() - wrappermap.add(self._renderer, self) - self.want_hover = False - - def set_control_size(self, size): - pass - - def setup_attributes(self, column, attr_map): - column.add_attribute(self._renderer, 'active', attr_map['value']) - -class GTKCustomCellRenderer(gtk.GenericCellRenderer): - """Handles the GTK hide of CustomCellRenderer - https://develop.participatoryculture.org/index.php/WidgetAPITableView""" - - def on_get_size(self, widget, cell_area=None): - wrapper = wrappermap.wrapper(self) - widget_wrapper = wrappermap.wrapper(widget) - style = drawing.DrawingStyle(widget_wrapper, use_base_color=True) - # NOTE: CustomCellRenderer.cell_data_func() sets up its attributes - # from the model itself, so we don't have to worry about setting them - # here. - width, height = wrapper.get_size(style, widget_wrapper.layout_manager) - x_offset = self.props.xpad - y_offset = self.props.ypad - width += self.props.xpad * 2 - height += self.props.ypad * 2 - if cell_area: - x_offset += cell_area.x - y_offset += cell_area.x - extra_width = max(0, cell_area.width - width) - extra_height = max(0, cell_area.height - height) - x_offset += int(round(self.props.xalign * extra_width)) - y_offset += int(round(self.props.yalign * extra_height)) - return x_offset, y_offset, width, height - - def on_render(self, window, widget, background_area, cell_area, expose_area, - flags): - widget_wrapper = wrappermap.wrapper(widget) - cell_wrapper = wrappermap.wrapper(self) - - selected = (flags & gtk.CELL_RENDERER_SELECTED) - if selected: - if widget.flags() & gtk.HAS_FOCUS: - state = gtk.STATE_SELECTED - else: - state = gtk.STATE_ACTIVE - else: - state = gtk.STATE_NORMAL - if cell_wrapper.IGNORE_PADDING: - area = background_area - else: - xpad = self.props.xpad - ypad = self.props.ypad - area = gtk.gdk.Rectangle(cell_area.x + xpad, cell_area.y + ypad, - cell_area.width - xpad * 2, cell_area.height - ypad * 2) - context = drawing.DrawingContext(window, area, expose_area) - if (selected and not widget_wrapper.draws_selection and - widget_wrapper.use_custom_style): - # Draw the base color as our background. This erases the gradient - # that GTK draws for selected items. - window.draw_rectangle(widget.style.base_gc[state], True, - background_area.x, background_area.y, - background_area.width, background_area.height) - context.style = drawing.DrawingStyle(widget_wrapper, - use_base_color=True, state=state) - widget_wrapper.layout_manager.update_cairo_context(context.context) - hotspot_tracker = widget_wrapper.hotspot_tracker - if (hotspot_tracker and hotspot_tracker.hit and - hotspot_tracker.column == self.column and - hotspot_tracker.path == self.path): - hotspot = hotspot_tracker.name - else: - hotspot = None - if (self.path, self.column) == widget_wrapper.hover_info: - hover = widget_wrapper.hover_pos - hover = (hover[0] - xpad, hover[1] - ypad) - else: - hover = None - # NOTE: CustomCellRenderer.cell_data_func() sets up its attributes - # from the model itself, so we don't have to worry about setting them - # here. - widget_wrapper.layout_manager.reset() - cell_wrapper.render(context, widget_wrapper.layout_manager, selected, - hotspot, hover) - - def on_activate(self, event, widget, path, background_area, cell_area, - flags): - pass - - def on_start_editing(self, event, widget, path, background_area, - cell_area, flags): - pass -gobject.type_register(GTKCustomCellRenderer) - -class CustomCellRenderer(object): - """Customizable Cell Renderer - https://develop.participatoryculture.org/index.php/WidgetAPITableView""" - - IGNORE_PADDING = False - - def __init__(self): - self._renderer = GTKCustomCellRenderer() - self.want_hover = False - wrappermap.add(self._renderer, self) - - def setup_attributes(self, column, attr_map): - column.set_cell_data_func(self._renderer, self.cell_data_func, - attr_map) - - def cell_data_func(self, column, cell, model, iter, attr_map): - cell.column = column - cell.path = model.get_path(iter) - row = model[iter] - # Set attributes on self instead cell This works because cell is just - # going to turn around and call our methods to do the rendering. - for name, index in attr_map.items(): - setattr(self, name, row[index]) - - def hotspot_test(self, style, layout, x, y, width, height): - return None - -class InfoListRenderer(CustomCellRenderer): - """Custom Renderer for InfoListModels - https://develop.participatoryculture.org/index.php/WidgetAPITableView""" - - def cell_data_func(self, column, cell, model, iter, attr_map): - self.info, self.attrs, self.group_info = \ - wrappermap.wrapper(model).row_for_iter(iter) - cell.column = column - cell.path = model.get_path(iter) - -class InfoListRendererText(CellRenderer): - """Renderer for InfoListModels that only display text - https://develop.participatoryculture.org/index.php/WidgetAPITableView""" - - def setup_attributes(self, column, attr_map): - infolist.gtk.setup_text_cell_data_func(column, self._renderer, - self.get_value) diff --git a/mvc/widgets/gtk/weakconnect.py b/mvc/widgets/gtk/weakconnect.py deleted file mode 100644 index b8b9526..0000000 --- a/mvc/widgets/gtk/weakconnect.py +++ /dev/null @@ -1,56 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -"""weakconnect.py -- Connect to a signal of a GObject using a weak method -reference. This means that this connection will not keep the object alive. -This is a good thing because it prevents circular references between wrapper -widgets and the wrapped GTK widget. -""" - -from mvc import signals - -class WeakSignalHandler(object): - def __init__(self, method): - self.method = signals.WeakMethodReference(method) - - def connect(self, obj, signal, *user_args): - self.user_args = user_args - self.signal_handle = obj.connect(signal, self.handle_callback) - return self.signal_handle - - def handle_callback(self, obj, *args): - real_method = self.method() - if real_method is not None: - return real_method(obj, *(args + self.user_args)) - else: - obj.disconnect(self.signal_handle) - -def weak_connect(gobject, signal, method, *user_args): - handler = WeakSignalHandler(method) - return handler.connect(gobject, signal, *user_args) diff --git a/mvc/widgets/gtk/widgets.py b/mvc/widgets/gtk/widgets.py deleted file mode 100644 index 6c4280d..0000000 --- a/mvc/widgets/gtk/widgets.py +++ /dev/null @@ -1,47 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -""".widgets -- Contains portable implementations of -the GTK Widgets. These are shared between the windows port and the x11 port. -""" - -import gtk - -# Just use the GDK Rectangle class -class Rect(gtk.gdk.Rectangle): - @classmethod - def from_string(cls, rect_string): - x, y, width, height = [int(i) for i in rect_string.split(',')] - return Rect(x, y, width, height) - - def __str__(self): - return "%d,%d,%d,%d" % (self.x, self.y, self.width, self.height) - - def get_width(self): - return self.width diff --git a/mvc/widgets/gtk/widgetset.py b/mvc/widgets/gtk/widgetset.py deleted file mode 100644 index c63855c..0000000 --- a/mvc/widgets/gtk/widgetset.py +++ /dev/null @@ -1,63 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -from .base import Widget, Bin -from .const import * -from .controls import TextEntry, NumberEntry, \ - SecureTextEntry, MultilineTextEntry, Checkbox, RadioButton, \ - RadioButtonGroup, OptionMenu, Button -from .customcontrols import ( - CustomButton, DragableCustomButton, CustomSlider, - ClickableImageButton) -# VolumeSlider and VolumeMuter aren't defined if gtk.VolumeButton -# doesn't have get_popup. -try: - from .customcontrols import ( - VolumeSlider, VolumeMuter) -except ImportError: - pass -from .contextmenu import ContextMenu -from .drawing import ImageSurface, DrawingContext, \ - DrawingArea, Background, Gradient -from .layout import HBox, VBox, Alignment, \ - Splitter, Table, TabContainer, DetachedWindowHolder -from .window import Window, MainWindow, Dialog, \ - FileOpenDialog, FileSaveDialog, DirectorySelectDialog, AboutDialog, \ - AlertDialog, DialogWindow -from .tableview import (TableView, TableModel, - TableColumn, TreeTableModel, CUSTOM_HEADER_HEIGHT) -from .tableviewcells import (CellRenderer, - ImageCellRenderer, CheckboxCellRenderer, CustomCellRenderer, - InfoListRenderer, InfoListRendererText) -from .simple import (Image, ImageDisplay, - AnimatedImageDisplay, Label, Scroller, Expander, SolidBackground, - ProgressBar, HLine) -from .widgets import Rect -from .gtkmenus import (MenuItem, RadioMenuItem, CheckMenuItem, Separator, - Menu, MenuBar, MainWindowMenuBar) diff --git a/mvc/widgets/gtk/window.py b/mvc/widgets/gtk/window.py deleted file mode 100644 index 3859a1a..0000000 --- a/mvc/widgets/gtk/window.py +++ /dev/null @@ -1,708 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Jesus Eduardo (Heckyel) | 2017 -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -""".window -- GTK Window widget.""" - -import gobject -import gtk -import os - -from mvc import resources -from mvc import signals - -import keymap -import layout -import widgets -import wrappermap - -# keeps the objects alive until destroy() is called -alive_windows = set() -running_dialogs = set() - -class WrappedWindow(gtk.Window): - def do_map(self): - gtk.Window.do_map(self) - wrappermap.wrapper(self).emit('show') - - def do_unmap(self): - gtk.Window.do_unmap(self) - wrappermap.wrapper(self).emit('hide') - def do_focus_in_event(self, event): - gtk.Window.do_focus_in_event(self, event) - wrappermap.wrapper(self).emit('active-change') - def do_focus_out_event(self, event): - gtk.Window.do_focus_out_event(self, event) - wrappermap.wrapper(self).emit('active-change') - - def do_key_press_event(self, event): - if self.activate_key(event): # event activated a menu item - return - - if self.propagate_key_event(event): # event handled by widget - return - - ret = keymap.translate_gtk_event(event) - if ret is not None: - key, modifiers = ret - rv = wrappermap.wrapper(self).emit('key-press', key, modifiers) - if not rv: - gtk.Window.do_key_press_event(self, event) - - def _get_focused_wrapper(self): - """Get the wrapper of the widget with keyboard focus""" - focused = self.get_focus() - # some of our widgets created children for their use - # (GtkSearchTextEntry). If we don't find a wrapper for - # focused, try it's parents - while focused is not None: - try: - wrapper = wrappermap.wrapper(focused) - except KeyError: - focused = focused.get_parent() - else: - return wrapper - return None - - def change_focus_using_wrapper(self, direction): - my_wrapper = wrappermap.wrapper(self) - focused_wrapper = self._get_focused_wrapper() - if direction == gtk.DIR_TAB_FORWARD: - to_focus = my_wrapper.get_next_tab_focus(focused_wrapper, True) - elif direction == gtk.DIR_TAB_BACKWARD: - to_focus = my_wrapper.get_next_tab_focus(focused_wrapper, False) - else: - return False - if to_focus is not None: - to_focus.focus() - return True - return False - - def do_focus(self, direction): - if not self.change_focus_using_wrapper(direction): - gtk.Window.do_focus(self, direction) - -gobject.type_register(WrappedWindow) - -class WindowBase(signals.SignalEmitter): - def __init__(self): - signals.SignalEmitter.__init__(self) - self.create_signal('use-custom-style-changed') - self.create_signal('key-press') - self.create_signal('show') - self.create_signal('hide') - - def set_window(self, window): - self._window = window - window.connect('style-set', self.on_style_set) - wrappermap.add(window, self) - self.calc_use_custom_style() - - def on_style_set(self, widget, old_style): - old_use_custom_style = self.use_custom_style - self.calc_use_custom_style() - if old_use_custom_style != self.use_custom_style: - self.emit('use-custom-style-changed') - - def calc_use_custom_style(self): - if self._window is not None: - base = self._window.style.base[gtk.STATE_NORMAL] - # Decide if we should use a custom style. Right now the - # formula is the base color is a very light shade of - # gray/white (lighter than #f0f0f0). - self.use_custom_style = ((base.red == base.green == base.blue) and - base.red >= 61680) - - -class Window(WindowBase): - """The main Libre window. """ - - def __init__(self, title, rect=None): - """Create the Libre Main Window. Title is the name to give the - window, rect specifies the position it should have on screen. - """ - WindowBase.__init__(self) - self.set_window(self._make_gtk_window()) - self._window.set_title(title) - self.setup_icon() - if rect: - self._window.set_default_size(rect.width, rect.height) - self._window.set_default_size(rect.width, rect.height) - self._window.set_gravity(gtk.gdk.GRAVITY_CENTER) - self._window.move(rect.x, rect.y) - - self.create_signal('active-change') - self.create_signal('will-close') - self.create_signal('did-move') - self.create_signal('file-drag-motion') - self.create_signal('file-drag-received') - self.create_signal('file-drag-leave') - self.create_signal('on-shown') - self.drag_signals = [] - alive_windows.add(self) - - self._window.connect('delete-event', self.on_delete_window) - self._window.connect('map-event', lambda w, a: self.emit('on-shown')) - # XXX: Define MVCWindow/MiroWindow style not hard code this - self._window.set_resizable(False) - - def setup_icon(self): - icon_pixbuf = gtk.gdk.pixbuf_new_from_file( - resources.image_path("mvc-logo.png")) - self._window.set_icon(icon_pixbuf) - - - def accept_file_drag(self, val): - if not val: - self._window.drag_dest_set(0, [], 0) - for handle in self.drag_signals: - self.disconnect(handle) - self.drag_signals = [] - else: - self._window.drag_dest_set( - gtk.DEST_DEFAULT_MOTION | gtk.DEST_DEFAULT_DROP, - [('text/uri-list', 0, 0)], - gtk.gdk.ACTION_COPY) - for signal, callback in ( - ('drag-motion', self.on_drag_motion), - ('drag-data-received', self.on_drag_data_received), - ('drag-leave', self.on_drag_leave)): - self.drag_signals.append( - self._window.connect(signal, callback)) - - def on_drag_motion(self, widget, context, x, y, time): - self.emit('file-drag-motion') - - def on_drag_data_received(self, widget, context, x, y, selection_data, - info, time): - self.emit('file-drag-received', selection_data.get_uris()) - - def on_drag_leave(self, widget, context, time): - self.emit('file-drag-leave') - - def on_delete_window(self, widget, event): - # when the user clicks on the X in the corner of the window we - # want that to close the window, but also trigger our - # will-close signal and all that machinery unless the window - # is currently hidden--then we don't do anything. - if not self._window.window.is_visible(): - return - self.close() - return True - - def _make_gtk_window(self): - return WrappedWindow() - - def set_title(self, title): - self._window.set_title(title) - - def get_title(self): - self._window.get_title() - - def center(self): - self._window.set_position(gtk.WIN_POS_CENTER) - - def show(self): - if self not in alive_windows: - raise ValueError("Window destroyed") - self._window.show() - - def close(self): - if hasattr(self, "_closing"): - return - self._closing = True - # Keep a reference to the widget in case will-close signal handler - # calls destroy() - old_window = self._window - self.emit('will-close') - old_window.hide() - del self._closing - - def destroy(self): - self.close() - self._window = None - alive_windows.discard(self) - - def is_active(self): - return self._window.is_active() - - def is_visible(self): - return self._window.props.visible - - def get_next_tab_focus(self, current, is_forward): - return None - - def set_content_widget(self, widget): - """Set the widget that will be drawn in the content area for this - window. - - It will be allocated the entire area of the widget, except the - space needed for the titlebar, frame and other decorations. - When the window is resized, content should also be resized. - """ - self._add_content_widget(widget) - widget._widget.show() - self.content_widget = widget - - def _add_content_widget(self, widget): - self._window.add(widget._widget) - - def get_content_widget(self, widget): - """Get the current content widget.""" - return self.content_widget - - def get_frame(self): - pos = self._window.get_position() - size = self._window.get_size() - return widgets.Rect(pos[0], pos[1], size[0], size[1]) - - def set_frame(self, x=None, y=None, width=None, height=None): - if x is not None or y is not None: - pos = self._window.get_position() - x = x if x is not None else pos[0] - y = y if y is not None else pos[1] - self._window.move(x, y) - - if width is not None or height is not None: - size = self._window.get_size() - width = width if width is not None else size[0] - height = height if height is not None else size[1] - self._window.resize(width, height) - - def get_monitor_geometry(self): - """Returns a Rect of the geometry of the monitor that this - window is currently on. - - :returns: Rect - """ - gtkwindow = self._window - gdkwindow = gtkwindow.window - screen = gtkwindow.get_screen() - - monitor = screen.get_monitor_at_window(gdkwindow) - return screen.get_monitor_geometry(monitor) - - def check_position_and_fix(self): - """This pulls the geometry of the monitor of the screen this - window is on as well as the position of the window. - - It then makes sure that the position y is greater than the - monitor geometry y. This makes sure that the titlebar of - the window is showing. - """ - gtkwindow = self._window - gdkwindow = gtkwindow.window - monitor_geom = self.get_monitor_geometry() - - frame_extents = gdkwindow.get_frame_extents() - position = gtkwindow.get_position() - - # if the frame is not visible, then we move the window so that - # it is - if frame_extents.y < monitor_geom.y: - gtkwindow.move(position[0], - monitor_geom.y + (position[1] - frame_extents.y)) - - - -class DialogWindow(Window): - def __init__(self, title, rect=None): - Window.__init__(self, title, rect) - self._window.set_resizable(False) - -class MainWindow(Window): - def __init__(self, title, rect): - Window.__init__(self, title, rect) - self.vbox = gtk.VBox() - self._window.add(self.vbox) - self.vbox.show() - self._add_app_menubar() - self.create_signal('save-dimensions') - self.create_signal('save-maximized') - self._window.connect('key-release-event', self.on_key_release) - self._window.connect('window-state-event', self.on_window_state_event) - self._window.connect('configure-event', self.on_configure_event) - - def _make_gtk_window(self): - return WrappedWindow() - - def on_delete_window(self, widget, event): - return True - - def on_configure_event(self, widget, event): - (x, y) = self._window.get_position() - (width, height) = self._window.get_size() - self.emit('save-dimensions', x, y, width, height) - - def on_window_state_event(self, widget, event): - maximized = bool( - event.new_window_state & gtk.gdk.WINDOW_STATE_MAXIMIZED) - self.emit('save-maximized', maximized) - - def on_key_release(self, widget, event): - if app.playback_manager.is_playing: - if gtk.gdk.keyval_name(event.keyval) in ('Right', 'Left', - 'Up', 'Down'): - return True - - def _add_app_menubar(self): - self.menubar = app.widgetapp.menubar - self.vbox.pack_start(self.menubar._widget, expand=False) - self.connect_menu_keyboard_shortcuts() - - def _add_content_widget(self, widget): - self.vbox.pack_start(widget._widget, expand=True) - - -class DialogBase(WindowBase): - def set_transient_for(self, window): - self._window.set_transient_for(window._window) - - def run(self): - running_dialogs.add(self) - try: - return self._run() - finally: - running_dialogs.remove(self) - self._window = None - - def _run(self): - """Run the dialog. Must be implemented by subclasses.""" - raise NotImplementedError() - - def destroy(self): - if self._window is not None: - self._window.response(gtk.RESPONSE_NONE) - # don't set self._window to None yet. We will unset it when we - # return from the _run() method - -class Dialog(DialogBase): - def __init__(self, title, description=None): - """Create a dialog.""" - DialogBase.__init__(self) - self.create_signal('open') - self.create_signal('close') - self.set_window(gtk.Dialog(title)) - self._window.set_default_size(425, -1) - self.extra_widget = None - self.buttons_to_add = [] - wrappermap.add(self._window, self) - self.description = description - - def build_content(self): - packing_vbox = layout.VBox(spacing=20) - packing_vbox._widget.set_border_width(6) - if self.description is not None: - label = gtk.Label(self.description) - label.set_line_wrap(True) - label.set_size_request(390, -1) - label.set_selectable(True) - packing_vbox._widget.pack_start(label) - if self.extra_widget: - packing_vbox._widget.pack_start(self.extra_widget._widget) - return packing_vbox - - def add_button(self, text): - from mvc.widgets import dialogs - _stock = { - dialogs.BUTTON_OK.text: gtk.STOCK_OK, - dialogs.BUTTON_CANCEL.text: gtk.STOCK_CANCEL, - dialogs.BUTTON_YES.text: gtk.STOCK_YES, - dialogs.BUTTON_NO.text: gtk.STOCK_NO, - dialogs.BUTTON_QUIT.text: gtk.STOCK_QUIT, - dialogs.BUTTON_REMOVE.text: gtk.STOCK_REMOVE, - dialogs.BUTTON_DELETE.text: gtk.STOCK_DELETE, - } - if text in _stock: - # store both the text and the stock ID - text = _stock[text], text - self.buttons_to_add.append(text) - - def pack_buttons(self): - # There's a couple tricky things here: - # 1) We need to add them in the reversed order we got them, since GTK - # lays them out left-to-right - # - # 2) We can't use 0 as a response-id. GTK only reserves positive - # response_ids for the user. - response_id = len(self.buttons_to_add) - for text in reversed(self.buttons_to_add): - label = None - if isinstance(text, tuple): # stock ID, text - text, label = text - button = self._window.add_button(text, response_id) - if label is not None: - button.set_label(label) - response_id -= 1 - self.buttons_to_add = [] - self._window.set_default_response(1) - - def _run(self): - self.pack_buttons() - packing_vbox = self.build_content() - self._window.vbox.pack_start(packing_vbox._widget, True, True) - self._window.show_all() - response = self._window.run() - self._window.hide() - if response == gtk.RESPONSE_DELETE_EVENT: - return -1 - else: - return response - 1 # response IDs started at 1 - - def set_extra_widget(self, widget): - self.extra_widget = widget - - def get_extra_widget(self): - return self.extra_widget - -class FileDialogBase(DialogBase): - def _run(self): - ret = self._window.run() - self._window.hide() - if ret == gtk.RESPONSE_OK: - self._files = self._window.get_filenames() - return 0 - -class FileOpenDialog(FileDialogBase): - def __init__(self, title): - FileDialogBase.__init__(self) - self._files = None - fcd = gtk.FileChooserDialog(title, - action=gtk.FILE_CHOOSER_ACTION_OPEN, - buttons=(gtk.STOCK_CANCEL, - gtk.RESPONSE_CANCEL, - gtk.STOCK_OPEN, - gtk.RESPONSE_OK)) - - self.set_window(fcd) - - def set_filename(self, text): - self._window.set_filename(text) - - def set_select_multiple(self, value): - self._window.set_select_multiple(value) - - def add_filters(self, filters): - for name, ext_list in filters: - f = gtk.FileFilter() - f.set_name(name) - for mem in ext_list: - f.add_pattern('*.%s' % mem) - self._window.add_filter(f) - - f = gtk.FileFilter() - f.set_name(_('All files')) - f.add_pattern('*') - self._window.add_filter(f) - - def get_filenames(self): - return [unicode(f) for f in self._files] - - def get_filename(self): - if self._files is None: - # clicked Cancel - return None - else: - return unicode(self._files[0]) - - # provide a common interface for file chooser dialogs - get_path = get_filename - def set_path(self, path): - # set_filename puts the whole path in the filename field - self._window.set_current_folder(os.path.dirname(path)) - self._window.set_current_name(os.path.basename(path)) - -class FileSaveDialog(FileDialogBase): - def __init__(self, title): - FileDialogBase.__init__(self) - self._files = None - fcd = gtk.FileChooserDialog(title, - action=gtk.FILE_CHOOSER_ACTION_SAVE, - buttons=(gtk.STOCK_CANCEL, - gtk.RESPONSE_CANCEL, - gtk.STOCK_SAVE, - gtk.RESPONSE_OK)) - self.set_window(fcd) - - def set_filename(self, text): - self._window.set_current_name(text) - - def get_filename(self): - if self._files is None: - # clicked Cancel - return None - else: - return unicode(self._files[0]) - - # provide a common interface for file chooser dialogs - get_path = get_filename - def set_path(self, path): - # set_filename puts the whole path in the filename field - self._window.set_current_folder(os.path.dirname(path)) - self._window.set_current_name(os.path.basename(path)) - -class DirectorySelectDialog(FileDialogBase): - def __init__(self, title): - FileDialogBase.__init__(self) - self._files = None - choose_str = 'Choose' - fcd = gtk.FileChooserDialog( - title, - action=gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER, - buttons=(gtk.STOCK_CANCEL, - gtk.RESPONSE_CANCEL, - choose_str, gtk.RESPONSE_OK)) - self.set_window(fcd) - - def set_directory(self, text): - self._window.set_filename(text) - - def get_directory(self): - if self._files is None: - # clicked Cancel - return None - else: - return unicode(self._files[0]) - - # provide a common interface for file chooser dialogs - get_path = get_directory - set_path = set_directory - -class AboutDialog(Dialog): - def __init__(self): - Dialog.__init__(self, "Libre Video Converter") -# _("About %(appname)s", -# {'appname': app.config.get(prefs.SHORT_APP_NAME)})) -# self.add_button(_("Close")) - self.add_button("Close") - self._window.set_has_separator(False) - - def build_content(self): - packing_vbox = layout.VBox(spacing=20) - #icon_pixbuf = gtk.gdk.pixbuf_new_from_file_at_size( - # resources.share_path('icons/hicolor/128x128/apps/miro.png'), - # 48, 48) - #packing_vbox._widget.pack_start(gtk.image_new_from_pixbuf(icon_pixbuf)) - #if app.config.get(prefs.APP_REVISION_NUM): - # version = "%s (%s)" % ( - # app.config.get(prefs.APP_VERSION), - # app.config.get(prefs.APP_REVISION_NUM)) - #else: - # version = "%s" % app.config.get(prefs.APP_VERSION) - version = '3.0' - #name_label = gtk.Label( - # '<span size="xx-large" weight="bold">%s %s</span>' % ( - # app.config.get(prefs.SHORT_APP_NAME), version)) - name_label = gtk.Label( - '<span size="xx-large" weight="bold">%s %s</span>' % ( - 'Libre Video Converter', version)) - name_label.set_use_markup(True) - packing_vbox._widget.pack_start(name_label) - copyright_text = 'Copyright (c) Jesus Eduardo (Heckyel) | 2017' - copyright_label = gtk.Label('<small>%s</small>' % copyright_text) - copyright_label.set_use_markup(True) - copyright_label.set_justify(gtk.JUSTIFY_CENTER) - packing_vbox._widget.pack_start(copyright_label) - - # FIXME - make the project url clickable - #packing_vbox._widget.pack_start( - # gtk.Label(app.config.get(prefs.PROJECT_URL))) - - #contributor_label = gtk.Label( - # _("Thank you to all the people who contributed to %(appname)s " - # "%(version)s:", - # {"appname": app.config.get(prefs.SHORT_APP_NAME), - # "version": app.config.get(prefs.APP_VERSION)})) - #contributor_label.set_justify(gtk.JUSTIFY_CENTER) - #packing_vbox._widget.pack_start(contributor_label) - - # get contributors, remove newlines and wrap it - #contributors = open(resources.path('CREDITS'), 'r').readlines() - #contributors = [c[2:].strip() - # for c in contributors if c.startswith("* ")] - #contributors = ", ".join(contributors) - - # show contributors - #contrib_buffer = gtk.TextBuffer() - #contrib_buffer.set_text(contributors) - - #contrib_view = gtk.TextView(contrib_buffer) - #contrib_view.set_editable(False) - #contrib_view.set_cursor_visible(False) - #contrib_view.set_wrap_mode(gtk.WRAP_WORD) - #contrib_window = gtk.ScrolledWindow() - #contrib_window.set_policy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS) - #contrib_window.add(contrib_view) - #contrib_window.set_size_request(-1, 100) - #packing_vbox._widget.pack_start(contrib_window) - - # FIXME - make the project url clickable - #donate_label = gtk.Label( - # _("To help fund continued %(appname)s development, visit the " - # "donation page at:", - # {"appname": app.config.get(prefs.SHORT_APP_NAME)})) - #donate_label.set_justify(gtk.JUSTIFY_CENTER) - #packing_vbox._widget.pack_start(donate_label) - - #packing_vbox._widget.pack_start( - # gtk.Label(app.config.get(prefs.DONATE_URL))) - return packing_vbox - - def on_contrib_link_event(self, texttag, widget, event, iter_): - if event.type == gtk.gdk.BUTTON_PRESS: - resources.open_url('https://notabug.org/heckyel/librevideoconverter') - -type_map = { - 0: gtk.MESSAGE_WARNING, - 1: gtk.MESSAGE_INFO, - 2: gtk.MESSAGE_ERROR -} - -class AlertDialog(DialogBase): - def __init__(self, title, description, alert_type): - DialogBase.__init__(self) - message_type = type_map.get(alert_type, gtk.MESSAGE_INFO) - self.set_window(gtk.MessageDialog(type=message_type, - message_format=description)) - self._window.set_title(title) - self.description = description - - def add_button(self, text): - self._window.add_button(_stock.get(text, text), 1) - self._window.set_default_response(1) - - def _run(self): - self._window.set_modal(False) - self._window.show_all() - response = self._window.run() - self._window.hide() - if response == gtk.RESPONSE_DELETE_EVENT: - return -1 - else: - # response IDs start at 1 - return response - 1 diff --git a/mvc/widgets/gtk/wrappermap.py b/mvc/widgets/gtk/wrappermap.py deleted file mode 100644 index c2b2aad..0000000 --- a/mvc/widgets/gtk/wrappermap.py +++ /dev/null @@ -1,50 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -""".wrappermap -- Map GTK Widgets to the Libre Widget -that wraps them. -""" - -import weakref - -# Maps gtk windows -> wrapper objects. We use a weak references to prevent -# circular references between the GTK widget and it's wrapper. (Keeping a -# reference to the GTK widget is fine, since if the wrapper is alive, the GTK -# widget should be). -widget_mapping = weakref.WeakValueDictionary() - -def wrapper(gtk_widget): - """Find the wrapper widget for a GTK widget.""" - try: - return widget_mapping[gtk_widget] - except KeyError: - raise KeyError("Widget wrapper no longer exists") - -def add(gtk_widget, wrapper): - widget_mapping[gtk_widget] = wrapper diff --git a/mvc/widgets/keyboard.py b/mvc/widgets/keyboard.py deleted file mode 100644 index 6700de2..0000000 --- a/mvc/widgets/keyboard.py +++ /dev/null @@ -1,69 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -"""Define keyboard input in a platform-independant way.""" - -(CTRL, ALT, SHIFT, CMD, MOD, RIGHT_ARROW, LEFT_ARROW, UP_ARROW, - DOWN_ARROW, SPACE, ENTER, DELETE, BKSPACE, ESCAPE, - F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12) = range(26) - -class Shortcut: - """Defines a shortcut key combination used to trigger this - menu item. - - The first argument is the shortcut key. Other arguments are - modifiers. - - Examples: - - >>> Shortcut("x", MOD) - >>> Shortcut(BKSPACE, MOD) - - This is wrong: - - >>> Shortcut(MOD, "x") - """ - def __init__(self, shortcut, *modifiers): - self.shortcut = shortcut - self.modifiers = modifiers - - def _get_key_symbol(self, value): - """Translate key values to their symbolic names.""" - if isinstance(self.shortcut, int): - shortcut_string = '<Unknown>' - for name, value in globals().iteritems(): - if value == self.shortcut: - return name - return repr(value) - - def __str__(self): - shortcut_string = self._get_key_symbol(self.shortcut) - mod_string = repr(set(self._get_key_symbol(k) for k in - self.modifiers)) - return "Shortcut(%s, %s)" % (shortcut_string, mod_string) diff --git a/mvc/widgets/menus.py b/mvc/widgets/menus.py deleted file mode 100644 index 62b0c68..0000000 --- a/mvc/widgets/menus.py +++ /dev/null @@ -1,268 +0,0 @@ -# menus.py -# -# Most of these are taken from libs/frontends/widgets/menus.py in the miro -# project. -# -# TODO: merge common bits! - -import collections - -from mvc import signals -from mvc.widgets import widgetutil -from mvc.widgets import widgetset -from mvc.widgets import app - -from mvc.widgets.keyboard import (Shortcut, CTRL, ALT, SHIFT, CMD, - MOD, RIGHT_ARROW, LEFT_ARROW, UP_ARROW, DOWN_ARROW, SPACE, ENTER, DELETE, - BKSPACE, ESCAPE, F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12) - -# XXX hack: - -def _(text, *params): - if params: - return text % params[0] - return text - -class MenuItem(widgetset.MenuItem): - """Portable MenuItem class. - - This adds group handling to the platform menu items. - """ - # group_map is used for the legacy menu updater code - group_map = collections.defaultdict(set) - - def __init__(self, label, name, shortcut=None, groups=None, - **state_labels): - widgetset.MenuItem.__init__(self, label, name, shortcut) - # state_labels is used for the legacy menu updater code - self.state_labels = state_labels - if groups: - if len(groups) > 1: - raise ValueError("only support one group") - MenuItem.group_map[groups[0]].add(self) - -class MenuItemFetcher(object): - """Get MenuItems by their name quickly. """ - - def __init__(self): - self._cache = {} - - def __getitem__(self, name): - if name in self._cache: - return self._cache[name] - else: - menu_item = app.widgetapp.menubar.find(name) - self._cache[name] = menu_item - return menu_item - -def get_app_menu(): - """Returns the default menu structure.""" - - app_name = "Libre Video Converter" # XXX HACK - - file_menu = widgetset.Menu(_("_File"), "FileMenu", [ - MenuItem(_("_Open"), "Open", Shortcut("o", MOD), - groups=["NonPlaying"]), - MenuItem(_("_Quit"), "Quit", Shortcut("q", MOD)), - ]) - help_menu = widgetset.Menu(_("_Help"), "HelpMenu", [ - MenuItem(_("About %(name)s", - {'name': app_name}), - "About") - ]) - - all_menus = [file_menu, help_menu] - return all_menus - -action_handlers = {} -group_action_handlers = {} - -def on_menubar_activate(menubar, action_name): - callback = lookup_handler(action_name) - if callback is not None: - callback() - -def lookup_handler(action_name): - """For a given action name, get a callback to handle it. Return - None if no callback is found. - """ - - retval = _lookup_group_handler(action_name) - if retval is None: - retval = action_handlers.get(action_name) - return retval - -def _lookup_group_handler(action_name): - try: - group_name, callback_arg = action_name.split('-', 1) - except ValueError: - return None # split return tuple of length 1 - try: - group_handler = group_action_handlers[group_name] - except KeyError: - return None - else: - return lambda: group_handler(callback_arg) - -def action_handler(name): - """Decorator for functions that handle menu actions.""" - def decorator(func): - action_handlers[name] = func - return func - return decorator - -def group_action_handler(action_prefix): - def decorator(func): - group_action_handlers[action_prefix] = func - return func - return decorator - -# File menu -@action_handler("Open") -def on_open(): - app.widgetapp.choose_file() - -@action_handler("Quit") -def on_quit(): - app.widgetapp.quit() - -# Help menu -@action_handler("About") -def on_about(): - app.widgetapp.about() - -class MenuManager(signals.SignalEmitter): - """Updates the menu based on the current selection. - - This includes enabling/disabling menu items, changing menu text - for plural selection and enabling/disabling the play button. The - play button is obviously not a menu item, but it's pretty closely - related - - Whenever code makes a change that could possibly affect which menu - items should be enabled/disabled, it should call the - update_menus() method. - - Signals: - - menus-updated(reasons): Emitted whenever update_menus() is called - """ - def __init__(self): - signals.SignalEmitter.__init__(self, 'menus-updated') - self.menu_item_fetcher = MenuItemFetcher() - #self.subtitle_encoding_updater = SubtitleEncodingMenuUpdater() - self.subtitle_encoding_updater = None - - def setup_menubar(self, menubar): - """Setup the main miro menubar. - """ - menubar.add_initial_menus(get_app_menu()) - menubar.connect("activate", on_menubar_activate) - self.menu_updaters = [] - - def _set_play_pause(self): - if ((not app.playback_manager.is_playing - or app.playback_manager.is_paused)): - label = _('Play') - else: - label = _('Pause') - self.menu_item_fetcher['PlayPauseItem'].set_label(label) - - def add_subtitle_encoding_menu(self, category_label, *encodings): - """Set up a subtitles encoding menu. - - This method should be called for each category of subtitle encodings - (East Asian, Western European, Unicode, etc). Pass it the list of - encodings for that category. - - :param category_label: human-readable name for the category - :param encodings: list of (label, encoding) tuples. label is a - human-readable name, and encoding is a value that we can pass to - VideoDisplay.select_subtitle_encoding() - """ - self.subtitle_encoding_updater.add_menu(category_label, encodings) - - def select_subtitle_encoding(self, encoding): - if not self.subtitle_encoding_updater.has_encodings(): - # OSX never sets up the subtitle encoding menu - return - menu_item_name = self.subtitle_encoding_updater.action_name(encoding) - try: - self.menu_item_fetcher[menu_item_name].set_state(True) - except KeyError: - logging.warn("Error enabling subtitle encoding menu item: %s", - menu_item_name) - - def update_menus(self, *reasons): - """Call this when a change is made that could change the menus - - Use reasons to describe why the menus could change. Some MenuUpdater - objects will do some optimizations based on that - """ - reasons = set(reasons) - self._set_play_pause() - for menu_updater in self.menu_updaters: - menu_updater.update(reasons) - self.emit('menus-updated', reasons) - -class MenuUpdater(object): - """Base class for objects that dynamically update menus.""" - def __init__(self, menu_name): - self.menu_name = menu_name - self.first_update = False - - # we lazily access our menu item, since we are created before the menubar - # is fully setup. - def get_menu(self): - try: - return self._menu - except AttributeError: - self._menu = app.widgetapp.menubar.find(self.menu_name) - return self._menu - menu = property(get_menu) - - def update(self, reasons): - if not self.first_update and not self.should_process_update(reasons): - return - self.first_update = False - self.start_update() - if not self.should_show_menu(): - self.menu.hide() - return - - self.menu.show() - if self.should_rebuild_menu(): - self.clear_menu() - self.populate_menu() - self.update_items() - - def should_process_update(self, reasons): - """Test if we should ignore the update call. - - :param reasons: the reasons passed in to MenuManager.update_menus() - """ - return True - - def clear_menu(self): - """Remove items from our menu before rebuilding it.""" - for child in self.menu.get_children(): - self.menu.remove(child) - - def start_update(self): - """Called at the very start of the update method. """ - pass - - def should_show_menu(self): - """Should we display the menu? """ - return True - - def should_rebuild_menu(self): - """Should we rebuild the menu structure?""" - return False - - def populate_menu(self): - """Add MenuItems to our menu.""" - pass - - def update_items(self): - """Update our menu items.""" - pass diff --git a/mvc/widgets/osx/Resources-Widgets/MainMenu.nib/designable.nib b/mvc/widgets/osx/Resources-Widgets/MainMenu.nib/designable.nib deleted file mode 100644 index b7fefd6..0000000 --- a/mvc/widgets/osx/Resources-Widgets/MainMenu.nib/designable.nib +++ /dev/null @@ -1,145 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<archive type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="8.00"> - <data> - <int key="IBDocument.SystemTarget">1060</int> - <string key="IBDocument.SystemVersion">12A269</string> - <string key="IBDocument.InterfaceBuilderVersion">2549</string> - <string key="IBDocument.AppKitVersion">1187</string> - <string key="IBDocument.HIToolboxVersion">624.00</string> - <object class="NSMutableDictionary" key="IBDocument.PluginVersions"> - <string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="NS.object.0">2549</string> - </object> - <array key="IBDocument.IntegratedClassDependencies"> - <string>NSCustomObject</string> - <string>NSMenu</string> - <string>NSMenuItem</string> - </array> - <array key="IBDocument.PluginDependencies"> - <string>com.apple.InterfaceBuilder.CocoaPlugin</string> - </array> - <object class="NSMutableDictionary" key="IBDocument.Metadata"> - <string key="NS.key.0">PluginDependencyRecalculationVersion</string> - <integer value="1" key="NS.object.0"/> - </object> - <array class="NSMutableArray" key="IBDocument.RootObjects" id="864178278"> - <object class="NSCustomObject" id="422340081"> - <object class="NSMutableString" key="NSClassName"> - <characters key="NS.bytes">NSApplication</characters> - </object> - </object> - <object class="NSCustomObject" id="99063961"> - <string key="NSClassName">FirstResponder</string> - </object> - <object class="NSCustomObject" id="399126242"> - <string key="NSClassName">NSApplication</string> - </object> - <object class="NSMenu" id="603720448"> - <string key="NSTitle">MainMenu</string> - <array class="NSMutableArray" key="NSMenuItems"> - <object class="NSMenuItem" id="726726549"> - <reference key="NSMenu" ref="603720448"/> - <string key="NSTitle">Libre Video Converter</string> - <string key="NSKeyEquiv"/> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <object class="NSCustomResource" key="NSOnImage"> - <string key="NSClassName">NSImage</string> - <string key="NSResourceName">NSMenuCheckmark</string> - </object> - <object class="NSCustomResource" key="NSMixedImage"> - <string key="NSClassName">NSImage</string> - <string key="NSResourceName">NSMenuMixedState</string> - </object> - <string key="NSAction">submenuAction:</string> - <object class="NSMenu" key="NSSubmenu" id="530441688"> - <string key="NSTitle">Libre Video Converter</string> - <array class="NSMutableArray" key="NSMenuItems"/> - <string key="NSName">_NSAppleMenu</string> - </object> - </object> - </array> - <string key="NSName">_NSMainMenu</string> - </object> - </array> - <object class="IBObjectContainer" key="IBDocument.Objects"> - <array class="NSMutableArray" key="connectionRecords"/> - <object class="IBMutableOrderedSet" key="objectRecords"> - <array key="orderedObjects"> - <object class="IBObjectRecord"> - <int key="objectID">0</int> - <array key="object" id="0"/> - <reference key="children" ref="864178278"/> - <nil key="parent"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">-2</int> - <reference key="object" ref="422340081"/> - <reference key="parent" ref="0"/> - <string key="objectName">File's Owner</string> - </object> - <object class="IBObjectRecord"> - <int key="objectID">-1</int> - <reference key="object" ref="99063961"/> - <reference key="parent" ref="0"/> - <string key="objectName">First Responder</string> - </object> - <object class="IBObjectRecord"> - <int key="objectID">29</int> - <reference key="object" ref="603720448"/> - <array class="NSMutableArray" key="children"> - <reference ref="726726549"/> - </array> - <reference key="parent" ref="0"/> - <string key="objectName">MainMenu</string> - </object> - <object class="IBObjectRecord"> - <int key="objectID">56</int> - <reference key="object" ref="726726549"/> - <array class="NSMutableArray" key="children"> - <reference ref="530441688"/> - </array> - <reference key="parent" ref="603720448"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">57</int> - <reference key="object" ref="530441688"/> - <reference key="parent" ref="726726549"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">-3</int> - <reference key="object" ref="399126242"/> - <reference key="parent" ref="0"/> - <string key="objectName">Application</string> - </object> - </array> - </object> - <dictionary class="NSMutableDictionary" key="flattenedProperties"> - <string key="-1.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="-2.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="-3.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="29.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="56.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="57.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - </dictionary> - <dictionary class="NSMutableDictionary" key="unlocalizedProperties"/> - <nil key="activeLocalization"/> - <dictionary class="NSMutableDictionary" key="localizations"/> - <nil key="sourceID"/> - <int key="maxID">248</int> - </object> - <object class="IBClassDescriber" key="IBDocument.Classes"/> - <int key="IBDocument.localizationMode">0</int> - <string key="IBDocument.TargetRuntimeIdentifier">IBCocoaFramework</string> - <object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDependencies"> - <string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin.macosx</string> - <real value="1060" key="NS.object.0"/> - </object> - <bool key="IBDocument.PluginDeclaredDependenciesTrackSystemTargetVersion">YES</bool> - <int key="IBDocument.defaultPropertyAccessControl">3</int> - <dictionary class="NSMutableDictionary" key="IBDocument.LastKnownImageSizes"> - <string key="NSMenuCheckmark">{11, 11}</string> - <string key="NSMenuMixedState">{10, 3}</string> - </dictionary> - </data> -</archive> diff --git a/mvc/widgets/osx/Resources-Widgets/MainMenu.nib/keyedobjects.nib b/mvc/widgets/osx/Resources-Widgets/MainMenu.nib/keyedobjects.nib Binary files differdeleted file mode 100644 index 963b444..0000000 --- a/mvc/widgets/osx/Resources-Widgets/MainMenu.nib/keyedobjects.nib +++ /dev/null diff --git a/mvc/widgets/osx/__init__.py b/mvc/widgets/osx/__init__.py deleted file mode 100644 index f227b35..0000000 --- a/mvc/widgets/osx/__init__.py +++ /dev/null @@ -1,74 +0,0 @@ -import sys - -from objc import * -from Foundation import * -from AppKit import * - -from PyObjCTools import AppHelper - -size_request_manager = None - -class AppController(NSObject): - def applicationDidFinishLaunching_(self, notification): - from mvc.widgets.osx.osxmenus import MenuBar - self.portableApp.menubar = MenuBar() - self.portableApp.startup() - self.portableApp.run() - - def setPortableApp_(self, portableApp): - self.portableApp = portableApp - - def handleMenuActivate_(self, menu_item): - from mvc.widgets.osx import osxmenus - osxmenus.handle_menu_activate(menu_item) - -def initialize(app): - nsapp = NSApplication.sharedApplication() - delegate = AppController.alloc().init() - delegate.setPortableApp_(app) - nsapp.setDelegate_(delegate) - - global size_request_manager - from mvc.widgets.osx.widgetupdates import SizeRequestManager - size_request_manager = SizeRequestManager() - - NSApplicationMain(sys.argv) - -def attach_menubar(): - pass - -def mainloop_start(): - pass - -def mainloop_stop(): - NSApplication.sharedApplication().terminate_(nil) - -def idle_add(callback, periodic=None): - def wrapper(): - callback() - if periodic is not None: - AppHelper.callLater(periodic, wrapper) - if periodic is not None and periodic < 0: - raise ValueError('periodic cannot be negative') - # XXX: we have a lousy thread API that doesn't allocate pools for us... - pool = NSAutoreleasePool.alloc().init() - if periodic is not None: - AppHelper.callLater(periodic, wrapper) - else: - AppHelper.callAfter(wrapper) - del pool - -def idle_remove(id_): - pass - -def reveal_file(filename): - # XXX: dumb lousy type conversions ... - path = NSURL.fileURLWithPath_(filename.decode('utf-8')).path() - NSWorkspace.sharedWorkspace().selectFile_inFileViewerRootedAtPath_( - path, nil) - -def get_conversion_directory(): - url, error = NSFileManager.defaultManager().URLForDirectory_inDomain_appropriateForURL_create_error_(NSMoviesDirectory, NSUserDomainMask, nil, YES, None) - if error: - return None - return url.path().encode('utf-8') diff --git a/mvc/widgets/osx/base.py b/mvc/widgets/osx/base.py deleted file mode 100644 index 913b372..0000000 --- a/mvc/widgets/osx/base.py +++ /dev/null @@ -1,367 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -""".base.py -- Widget base classes.""" - -from AppKit import * -from Foundation import * -from objc import YES, NO, nil - -from mvc import signals -import wrappermap -from .viewport import Viewport, BorrowedViewport - -class Widget(signals.SignalEmitter): - """Base class for Cocoa widgets. - - attributes: - - CREATES_VIEW -- Does the widget create a view for itself? If this is True - the widget must have an attribute named view, which is the view that the - widget uses. - - placement -- What portion of view the widget occupies. - """ - - CREATES_VIEW = True - - def __init__(self): - signals.SignalEmitter.__init__(self, 'size-request-changed', - 'size-allocated', 'key-press', 'focus-out') - self.create_signal('place-in-scroller') - self.viewport = None - self.parent_is_scroller = False - self.manual_size_request = None - self.cached_size_request = None - self._disabled = False - - def set_can_focus(self, allow): - assert isinstance(self.view, NSControl) - self.view.setRefusesFirstResponder_(not allow) - - def set_size_request(self, width, height): - self.manual_size_request = (width, height) - self.invalidate_size_request() - - def clear_size_request_cache(self): - from mvc.widgets.osx import size_request_manager - if size_request_manager is not None: - while size_request_manager.widgets_to_request: - size_request_manager._run_requests() - - def get_size_request(self): - if self.manual_size_request: - width, height = self.manual_size_request - if width == -1: - width = self.get_natural_size_request()[0] - if height == -1: - height = self.get_natural_size_request()[1] - return width, height - return self.get_natural_size_request() - - def get_natural_size_request(self): - if self.cached_size_request: - return self.cached_size_request - else: - self.cached_size_request = self.calc_size_request() - return self.cached_size_request - - def invalidate_size_request(self): - from mvc.widgets.osx import size_request_manager - if size_request_manager is not None: - size_request_manager.add_widget(self) - - def do_invalidate_size_request(self): - """Recalculate the size request for this widget.""" - old_size_request = self.cached_size_request - self.cached_size_request = None - self.emit('size-request-changed', old_size_request) - - def calc_size_request(self): - """Return the minimum size needed to display this widget. - Must be Implemented by subclasses. - """ - raise NotImplementedError() - - def _debug_size_request(self, nesting_level=0): - """Debug size request calculations. - - This method recursively prints out the size request for each widget. - """ - request = self.calc_size_request() - width = int(request[0]) - height = int(request[1]) - indent = ' ' * nesting_level - me = str(self.__class__).split('.')[-1] - print '%s%s: %sx%s' % (indent, me, width, height) - - def place(self, rect, containing_view): - """Place this widget on a view. """ - if self.viewport is None: - if self.CREATES_VIEW: - self.viewport = Viewport(self.view, rect) - containing_view.addSubview_(self.view) - wrappermap.add(self.view, self) - else: - self.viewport = BorrowedViewport(containing_view, rect) - self.viewport_created() - else: - if not self.viewport.at_position(rect): - self.viewport.reposition(rect) - self.viewport_repositioned() - self.emit('size-allocated', rect.size.width, rect.size.height) - - def remove_viewport(self): - if self.viewport is not None: - self.viewport.remove() - self.viewport = None - if self.CREATES_VIEW: - wrappermap.remove(self.view) - - def viewport_created(self): - """Called after we first create a viewport. Subclasses can override - this method if they want to handle this event. - """ - - def viewport_repositioned(self): - """Called when we reposition our viewport. Subclasses can override - this method if they want to handle this event. - """ - - def viewport_scrolled(self): - """Called by the Scroller widget on it's child widget when it is - scrolled. - """ - - def get_width(self): - return int(self.viewport.get_width()) - width = property(get_width) - - def get_height(self): - return int(self.viewport.get_height()) - height = property(get_height) - - def get_window(self): - if not self.viewport.view: - return None - return wrappermap.wrapper(self.viewport.view.window()) - - def queue_redraw(self): - if self.viewport: - self.viewport.queue_redraw() - - def redraw_now(self): - if self.viewport: - self.viewport.redraw_now() - - def relative_position(self, other_widget): - """Get the position of another widget, relative to this widget.""" - basePoint = self.viewport.view.convertPoint_fromView_( - other_widget.viewport.area().origin, - other_widget.viewport.view) - return (basePoint.x - self.viewport.area().origin.x, - basePoint.y - self.viewport.area().origin.y) - - def make_color(self, (red, green, blue)): - return NSColor.colorWithDeviceRed_green_blue_alpha_(red, green, blue, - 1.0) - - def enable(self): - self._disabled = False - - def disable(self): - self._disabled = True - - def set_disabled(self, disabled): - if disabled: - self.disable() - else: - self.enable() - - def get_disabled(self): - return self._disabled - -class Container(Widget): - """Widget that holds other widgets. """ - - def __init__(self): - Widget.__init__(self) - self.callback_handles = {} - - def on_child_size_request_changed(self, child, old_size): - self.invalidate_size_request() - - def connect_child_signals(self, child): - handle = child.connect_weak('size-request-changed', - self.on_child_size_request_changed) - self.callback_handles[child] = handle - - def disconnect_child_signals(self, child): - child.disconnect(self.callback_handles.pop(child)) - - def remove_viewport(self): - for child in self.children: - child.remove_viewport() - Widget.remove_viewport(self) - - def child_added(self, child): - """Must be called by subclasses when a child is added to the - Container.""" - self.connect_child_signals(child) - self.children_changed() - - def child_removed(self, child): - """Must be called by subclasses when a child is removed from the - Container.""" - self.disconnect_child_signals(child) - child.remove_viewport() - self.children_changed() - - def child_changed(self, old_child, new_child): - """Must be called by subclasses when a child is replaced by a new - child in the Container. To simplify things a bit for subclasses, - old_child can be None in which case this is the same as - child_added(new_child). - """ - if old_child is not None: - self.disconnect_child_signals(old_child) - old_child.remove_viewport() - self.connect_child_signals(new_child) - self.children_changed() - - def children_changed(self): - """Invoked when the set of children for this widget changes.""" - self.do_invalidate_size_request() - - def do_invalidate_size_request(self): - Widget.do_invalidate_size_request(self) - if self.viewport: - self.place_children() - - def viewport_created(self): - self.place_children() - - def viewport_repositioned(self): - self.place_children() - - def viewport_scrolled(self): - for child in self.children: - child.viewport_scrolled() - - def place_children(self): - """Layout our child widgets. Must be implemented by subclasses.""" - raise NotImplementedError() - - def _debug_size_request(self, nesting_level=0): - for child in self.children: - child._debug_size_request(nesting_level+1) - Widget._debug_size_request(self, nesting_level) - -class Bin(Container): - """Container that only has one child widget.""" - - def __init__(self, child=None): - Container.__init__(self) - self.child = None - if child is not None: - self.add(child) - - def get_children(self): - if self.child: - return [self.child] - else: - return [] - children = property(get_children) - - def add(self, child): - if self.child is not None: - raise ValueError("Already have a child: %s" % self.child) - self.child = child - self.child_added(self.child) - - def remove(self): - if self.child is not None: - old_child = self.child - self.child = None - self.child_removed(old_child) - - def set_child(self, new_child): - old_child = self.child - self.child = new_child - self.child_changed(old_child, new_child) - - def enable(self): - Container.enable(self) - self.child.enable() - - def disable(self): - Container.disable(self) - self.child.disable() - -class SimpleBin(Bin): - """Bin that whose child takes up it's entire space.""" - - def calc_size_request(self): - if self.child is None: - return (0, 0) - else: - return self.child.get_size_request() - - def place_children(self): - if self.child: - self.child.place(self.viewport.area(), self.viewport.view) - -class FlippedView(NSView): - """Flipped NSView. We use these internally to lessen the differences - between Cocoa and GTK. - """ - - def init(self): - self = super(FlippedView, self).init() - self.background = None - return self - - def initWithFrame_(self, rect): - self = super(FlippedView, self).initWithFrame_(rect) - self.background = None - return self - - def isFlipped(self): - return YES - - def isOpaque(self): - return self.background is not None - - def setBackgroundColor_(self, color): - self.background = color - - def drawRect_(self, rect): - if self.background: - self.background.set() - NSBezierPath.fillRect_(rect) diff --git a/mvc/widgets/osx/const.py b/mvc/widgets/osx/const.py deleted file mode 100644 index ae0da40..0000000 --- a/mvc/widgets/osx/const.py +++ /dev/null @@ -1,44 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -from AppKit import * - -"""const.py -- Constants""" - -DRAG_ACTION_NONE = NSDragOperationNone -DRAG_ACTION_COPY = NSDragOperationCopy -DRAG_ACTION_MOVE = NSDragOperationMove -DRAG_ACTION_LINK = NSDragOperationLink -DRAG_ACTION_ALL = (DRAG_ACTION_COPY | DRAG_ACTION_MOVE | DRAG_ACTION_LINK) - -ITEM_TITLE_FONT = "Helvetica" -ITEM_DESC_FONT = "Helvetica" -ITEM_INFO_FONT = "Lucida Grande" - -TOOLBAR_GRAY = (0.19, 0.19, 0.19) diff --git a/mvc/widgets/osx/contextmenu.py b/mvc/widgets/osx/contextmenu.py deleted file mode 100644 index 7a8fa55..0000000 --- a/mvc/widgets/osx/contextmenu.py +++ /dev/null @@ -1,84 +0,0 @@ -from AppKit import * -from objc import nil - -from .base import Widget - -class ContextMenuHandler(NSObject): - def initWithCallback_widget_i_(self, callback, widget, i): - self = super(ContextMenuHandler, self).init() - self.callback = callback - self.widget = widget - self.i = i - return self - - def handleMenuItem_(self, sender): - self.callback(self.widget, self.i) - - -class MiroContextMenu(NSMenu): - # Works exactly like NSMenu, except it keeps a reference to the menu - # handler objects. - def init(self): - self = super(MiroContextMenu, self).init() - self.handlers = set() - return self - - def addItem_(self, item): - if isinstance(item.target(), ContextMenuHandler): - self.handlers.add(item.target()) - return NSMenu.addItem_(self, item) - - -class ContextMenu(object): - - def __init__(self, options): - super(ContextMenu, self).__init__() - self.menu = MiroContextMenu.alloc().init() - for i, item_info in enumerate(options): - if item_info is None: - nsitem = NSMenuItem.separatorItem() - else: - label, callback = item_info - nsitem = NSMenuItem.alloc().init() - font_size = NSFont.systemFontSize() - font = NSFont.fontWithName_size_("Lucida Sans Italic", font_size) - if font is None: - font = NSFont.systemFontOfSize_(font_size) - attributes = {NSFontAttributeName: font} - attributed_label = NSAttributedString.alloc().initWithString_attributes_(label, attributes) - nsitem.setAttributedTitle_(attributed_label) - else: - nsitem.setTitle_(label) - if isinstance(callback, list): - submenu = ContextMenu(callback) - self.menu.setSubmenu_forItem_(submenu.menu, nsitem) - else: - handler = ContextMenuHandler.alloc().initWithCallback_widget_i_(callback, self, i) - nsitem.setTarget_(handler) - nsitem.setAction_('handleMenuItem:') - self.menu.addItem_(nsitem) - - def popup(self): - # support for non-window based popups thanks to - # http://stackoverflow.com/questions/9033534/how-can-i-pop-up-nsmenu-at-mouse-cursor-position - location = NSEvent.mouseLocation() - frame = NSMakeRect(location.x, location.y, 200, 200) - window = NSWindow.alloc().initWithContentRect_styleMask_backing_defer_( - frame, - NSBorderlessWindowMask, - NSBackingStoreBuffered, - NO) - window.setAlphaValue_(0) - window.makeKeyAndOrderFront_(NSApp) - location_in_window = window.convertScreenToBase_(location) - event = NSEvent.mouseEventWithType_location_modifierFlags_timestamp_windowNumber_context_eventNumber_clickCount_pressure_( - NSLeftMouseDown, - location_in_window, - 0, - 0, - window.windowNumber(), - nil, - 0, - 0, - 0) - NSMenu.popUpContextMenu_withEvent_forView_(self.menu, event, window.contentView()) diff --git a/mvc/widgets/osx/control.py b/mvc/widgets/osx/control.py deleted file mode 100644 index ed6ea34..0000000 --- a/mvc/widgets/osx/control.py +++ /dev/null @@ -1,530 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -""".control - Controls.""" - -from AppKit import * -from Foundation import * -from objc import YES, NO, nil - -from mvc.widgets import widgetconst -import wrappermap -from .base import Widget -from .helpers import NotificationForwarder - -class SizedControl(Widget): - def set_size(self, size): - if size == widgetconst.SIZE_NORMAL: - self.view.cell().setControlSize_(NSRegularControlSize) - font = NSFont.systemFontOfSize_(NSFont.systemFontSize()) - self.font_size = NSFont.systemFontSize() - elif size == widgetconst.SIZE_SMALL: - font = NSFont.systemFontOfSize_(NSFont.smallSystemFontSize()) - self.view.cell().setControlSize_(NSSmallControlSize) - self.font_size = NSFont.smallSystemFontSize() - else: - self.view.cell().setControlSize_(NSRegularControlSize) - font = NSFont.systemFontOfSize_(NSFont.systemFontSize() * size) - self.font_size = NSFont.systemFontSize() * size - self.view.setFont_(font) - -class BaseTextEntry(SizedControl): - """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class.""" - def __init__(self, initial_text=None): - SizedControl.__init__(self) - self.view = self.make_view() - self.font = NSFont.systemFontOfSize_(NSFont.systemFontSize()) - self.view.setFont_(self.font) - self.view.setEditable_(YES) - self.view.cell().setScrollable_(YES) - self.view.cell().setLineBreakMode_(NSLineBreakByClipping) - self.sizer_cell = self.view.cell().copy() - if initial_text: - self.view.setStringValue_(initial_text) - self.set_width(len(initial_text)) - else: - self.set_width(10) - - self.notifications = NotificationForwarder.create(self.view) - - self.create_signal('activate') - self.create_signal('changed') - self.create_signal('validate') - - def focus(self): - if self.view.window() is not None: - self.view.window().makeFirstResponder_(self.view) - - def start_editing(self, initial_text): - self.set_text(initial_text) - self.focus() - # unselect the text and locate the cursor at the end of the entry - text_field = self.view.window().fieldEditor_forObject_(YES, self.view) - text_field.setSelectedRange_(NSMakeRange(len(self.get_text()), 0)) - - def viewport_created(self): - SizedControl.viewport_created(self) - self.notifications.connect(self.on_changed, 'NSControlTextDidChangeNotification') - self.notifications.connect(self.on_end_editing, - 'NSControlTextDidEndEditingNotification') - - def remove_viewport(self): - SizedControl.remove_viewport(self) - self.notifications.disconnect() - - def baseline(self): - return -self.view.font().descender() + 2 - - def on_changed(self, notification): - self.emit('changed') - - def on_end_editing(self, notification): - self.emit('focus-out') - - def calc_size_request(self): - size = self.sizer_cell.cellSize() - return size.width, size.height - - def set_text(self, text): - self.view.setStringValue_(text) - self.emit('changed') - - def get_text(self): - return self.view.stringValue() - - def set_width(self, chars): - self.sizer_cell.setStringValue_('X' * chars) - self.invalidate_size_request() - - def set_activates_default(self, setting): - pass - - def enable(self): - SizedControl.enable(self) - self.view.setEnabled_(True) - - def disable(self): - SizedControl.disable(self) - self.view.setEnabled_(False) - -class MiroTextField(NSTextField): - def textDidEndEditing_(self, notification): - wrappermap.wrapper(self).emit('activate') - return NSTextField.textDidEndEditing_(self, notification) - -class TextEntry(BaseTextEntry): - def make_view(self): - return MiroTextField.alloc().init() - -class NumberEntry(BaseTextEntry): - def make_view(self): - return MiroTextField.alloc().init() - - def set_max_length(self, length): - # TODO - pass - - def _filter_value(self): - """Discard any non-numeric characters""" - digits = ''.join(x for x in self.view.stringValue() if x.isdigit()) - self.view.setStringValue_(digits) - - def on_changed(self, notification): - # overriding on_changed rather than connecting to it ensures that we - # filter the value before anything else connected to the signal sees it - self._filter_value() - BaseTextEntry.on_changed(self, notification) - - def get_text(self): - # handles get_text between when text is entered and when on_changed - # filters it, in case that's possible - self._filter_value() - return BaseTextEntry.get_text(self) - -class MiroSecureTextField(NSSecureTextField): - def textDidEndEditing_(self, notification): - wrappermap.wrapper(self).emit('activate') - return NSSecureTextField.textDidEndEditing_(self, notification) - -class SecureTextEntry(BaseTextEntry): - def make_view(self): - return MiroSecureTextField.alloc().init() - -class MultilineTextEntry(Widget): - def __init__(self, initial_text=None): - Widget.__init__(self) - if initial_text is None: - initial_text = "" - self.view = NSTextView.alloc().initWithFrame_(NSRect((0,0),(50,50))) - self.view.setMaxSize_((1.0e7, 1.0e7)) - self.view.setHorizontallyResizable_(NO) - self.view.setVerticallyResizable_(YES) - self.notifications = NotificationForwarder.create(self.view) - self.create_signal('changed') - self.create_signal('focus-out') - if initial_text is not None: - self.set_text(initial_text) - self.set_size(widgetconst.SIZE_NORMAL) - - def set_size(self, size): - if size == widgetconst.SIZE_NORMAL: - font = NSFont.systemFontOfSize_(NSFont.systemFontSize()) - elif size == widgetconst.SIZE_SMALL: - self.view.cell().setControlSize_(NSSmallControlSize) - else: - raise ValueError("Unknown size: %s" % size) - self.view.setFont_(font) - - def viewport_created(self): - Widget.viewport_created(self) - self.notifications.connect(self.on_changed, 'NSTextDidChangeNotification') - self.notifications.connect(self.on_end_editing, - 'NSControlTextDidEndEditingNotification') - self.invalidate_size_request() - - def remove_viewport(self): - Widget.remove_viewport(self) - self.notifications.disconnect() - - def focus(self): - if self.view.window() is not None: - self.view.window().makeFirstResponder_(self.view) - - def set_text(self, text): - self.view.setString_(text) - self.invalidate_size_request() - - def get_text(self): - return self.view.string() - - def on_changed(self, notification): - self.invalidate_size_request() - self.emit("changed") - - def on_end_editing(self, notification): - self.emit("focus-out") - - def calc_size_request(self): - layout_manager = self.view.layoutManager() - text_container = self.view.textContainer() - # The next line is there just to force cocoa to layout the text - layout_manager.glyphRangeForTextContainer_(text_container) - rect = layout_manager.usedRectForTextContainer_(text_container) - return rect.size.width, rect.size.height - - def set_editable(self, editable): - if editable: - self.view.setEditable_(YES) - else: - self.view.setEditable_(NO) - - -class MiroButton(NSButton): - - def initWithSignal_(self, signal): - self = super(MiroButton, self).init() - self.signal = signal - return self - - def sendAction_to_(self, action, to): - # We override the Cocoa machinery here and just send it to our wrapper - # widget. - wrappermap.wrapper(self).emit(self.signal) - return YES - -class Checkbox(SizedControl): - """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class.""" - def __init__(self, text="", bold=False, color=None): - SizedControl.__init__(self) - self.create_signal('toggled') - self.view = MiroButton.alloc().initWithSignal_('toggled') - self.view.setButtonType_(NSSwitchButton) - self.bold = bold - self.title = text - self.font_size = NSFont.systemFontSize() - self.color = self.make_color(color) - self._set_title() - - def set_size(self, size): - SizedControl.set_size(self, size) - self._set_title() - - def _set_title(self): - if self.color is None: - self.view.setTitle_(self.title) - else: - attributes = { - NSForegroundColorAttributeName: self.color, - NSFontAttributeName: NSFont.systemFontOfSize_(self.font_size) - } - string = NSAttributedString.alloc().initWithString_attributes_( - self.title, attributes) - self.view.setAttributedTitle_(string) - - def calc_size_request(self): - if self.manual_size_request: - width, height = self.manual_size_request - if width == -1: - width = 10000 - if height == -1: - height = 10000 - size = self.view.cell().cellSizeForBounds_( - NSRect((0, 0), (width, height))) - else: - size = self.view.cell().cellSize() - return (size.width, size.height) - - def baseline(self): - return -self.view.font().descender() + 1 - - def get_checked(self): - return self.view.state() == NSOnState - - def set_checked(self, value): - if value: - self.view.setState_(NSOnState) - else: - self.view.setState_(NSOffState) - - def enable(self): - SizedControl.enable(self) - self.view.setEnabled_(True) - - def disable(self): - SizedControl.disable(self) - self.view.setEnabled_(False) - - def get_text_padding(self): - """ - Returns the amount of space the checkbox takes up before the label. - """ - # XXX FIXME - return 18 - -class Button(SizedControl): - """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class.""" - def __init__(self, label, style='normal', width=0): - SizedControl.__init__(self) - self.color = None - self.title = label - self.create_signal('clicked') - self.view = MiroButton.alloc().initWithSignal_('clicked') - self.view.setButtonType_(NSMomentaryPushInButton) - self._set_title() - self.setup_style(style) - self.min_width = width - - def set_text(self, label): - self.title = label - self._set_title() - - def set_color(self, color): - self.color = self.make_color(color) - self._set_title() - - def _set_title(self): - if self.color is None: - self.view.setTitle_(self.title) - else: - attributes = { - NSForegroundColorAttributeName: self.color, - NSFontAttributeName: self.view.font() - } - string = NSAttributedString.alloc().initWithString_attributes_( - self.title, attributes) - self.view.setAttributedTitle_(string) - - def setup_style(self, style): - if style == 'normal': - self.view.setBezelStyle_(NSRoundedBezelStyle) - self.pad_height = 0 - self.pad_width = 10 - self.min_width = 112 - elif style == 'smooth': - self.view.setBezelStyle_(NSRoundRectBezelStyle) - self.pad_width = 0 - self.pad_height = 4 - self.paragraph_style = NSMutableParagraphStyle.alloc().init() - self.paragraph_style.setAlignment_(NSCenterTextAlignment) - - def make_default(self): - self.view.setKeyEquivalent_("\r") - - def calc_size_request(self): - size = self.view.cell().cellSize() - width = max(self.min_width, size.width + self.pad_width) - height = size.height + self.pad_height - return width, height - - def baseline(self): - return -self.view.font().descender() + 10 + self.pad_height - - def enable(self): - SizedControl.enable(self) - self.view.setEnabled_(True) - - def disable(self): - SizedControl.disable(self) - self.view.setEnabled_(False) - -class MiroPopupButton(NSPopUpButton): - - def init(self): - self = super(MiroPopupButton, self).init() - self.setTarget_(self) - self.setAction_('handleChange:') - return self - - def handleChange_(self, sender): - wrappermap.wrapper(self).emit('changed', self.indexOfSelectedItem()) - -class OptionMenu(SizedControl): - def __init__(self, options): - SizedControl.__init__(self) - self.create_signal('changed') - self.view = MiroPopupButton.alloc().init() - self.options = options - for option, value in options: - self.view.addItemWithTitle_(option) - - def baseline(self): - if self.view.cell().controlSize() == NSRegularControlSize: - return -self.view.font().descender() + 6 - else: - return -self.view.font().descender() + 5 - - def calc_size_request(self): - return self.view.cell().cellSize() - - def set_selected(self, index): - self.view.selectItemAtIndex_(index) - - def get_selected(self): - return self.view.indexOfSelectedItem() - - def enable(self): - SizedControl.enable(self) - self.view.setEnabled_(True) - - def disable(self): - SizedControl.disable(self) - self.view.setEnabled_(False) - - def set_width(self, width): - # TODO - pass - -class RadioButtonGroup: - def __init__(self): - self._buttons = [] - - def handle_click(self, widget): - self.set_selected(widget) - - def add_button(self, button): - self._buttons.append(button) - button.connect('clicked', self.handle_click) - if len(self._buttons) == 1: - button.view.setState_(NSOnState) - else: - button.view.setState_(NSOffState) - - def get_buttons(self): - return self._buttons - - def get_selected(self): - for mem in self._buttons: - if mem.get_selected(): - return mem - - def set_selected(self, button): - for mem in self._buttons: - if button is mem: - mem.view.setState_(NSOnState) - else: - mem.view.setState_(NSOffState) - -class RadioButton(SizedControl): - def __init__(self, label, group=None, bold=False, color=None): - SizedControl.__init__(self) - self.create_signal('clicked') - self.view = MiroButton.alloc().initWithSignal_('clicked') - self.view.setButtonType_(NSRadioButton) - self.color = self.make_color(color) - self.title = label - self.bold = bold - self.font_size = NSFont.systemFontSize() - self._set_title() - - if group is not None: - self.group = group - else: - self.group = RadioButtonGroup() - - self.group.add_button(self) - - def set_size(self, size): - SizedControl.set_size(self, size) - self._set_title() - - def _set_title(self): - if self.color is None: - self.view.setTitle_(self.title) - else: - attributes = { - NSForegroundColorAttributeName: self.color, - NSFontAttributeName: NSFont.systemFontOfSize_(self.font_size) - } - string = NSAttributedString.alloc().initWithString_attributes_( - self.title, attributes) - self.view.setAttributedTitle_(string) - - def calc_size_request(self): - size = self.view.cell().cellSize() - return (size.width, size.height) - - def baseline(self): - -self.view.font().descender() + 2 - - def get_group(self): - return self.group - - def get_selected(self): - return self.view.state() == NSOnState - - def set_selected(self): - self.group.set_selected(self) - - def enable(self): - SizedControl.enable(self) - self.view.setEnabled_(True) - - def disable(self): - SizedControl.disable(self) - self.view.setEnabled_(False) diff --git a/mvc/widgets/osx/customcontrol.py b/mvc/widgets/osx/customcontrol.py deleted file mode 100644 index d100f33..0000000 --- a/mvc/widgets/osx/customcontrol.py +++ /dev/null @@ -1,436 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -""".customcontrol -- CustomControl handlers. """ - -import collections - -from AppKit import * -from Foundation import * -from objc import YES, NO, nil - -from mvc.widgets import widgetconst -import wrappermap -from .base import Widget -import drawing -from .layoutmanager import LayoutManager - -class DrawableButtonCell(NSButtonCell): - def startTrackingAt_inView_(self, point, view): - view.setState_(NSOnState) - return YES - - def continueTracking_at_inView_(self, lastPoint, at, view): - view.setState_(NSOnState) - return YES - - def stopTracking_at_inView_mouseIsUp_(self, lastPoint, at, view, mouseIsUp): - if not mouseIsUp: - view.mouse_inside = False - view.setState_(NSOffState) - -class DrawableButton(NSButton): - def init(self): - self = super(DrawableButton, self).init() - self.layout_manager = LayoutManager() - self.tracking_area = None - self.mouse_inside = False - self.custom_cursor = None - return self - - def resetCursorRects(self): - if self.custom_cursor is not None: - self.addCursorRect_cursor_(self.visibleRect(), self.custom_cursor) - self.custom_cursor.setOnMouseEntered_(YES) - - def updateTrackingAreas(self): - # remove existing tracking area if needed - if self.tracking_area: - self.removeTrackingArea_(self.tracking_area) - - # create a new tracking area for the entire view. This allows us to - # get mouseMoved events whenever the mouse is inside our view. - self.tracking_area = NSTrackingArea.alloc() - self.tracking_area.initWithRect_options_owner_userInfo_( - self.visibleRect(), - NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | - NSTrackingActiveInKeyWindow, - self, - nil) - self.addTrackingArea_(self.tracking_area) - - def mouseEntered_(self, event): - window = self.window() - if window is not nil and window.isMainWindow(): - self.mouse_inside = True - self.setNeedsDisplay_(YES) - - def mouseExited_(self, event): - window = self.window() - if window is not nil and window.isMainWindow(): - self.mouse_inside = False - self.setNeedsDisplay_(YES) - - def isOpaque(self): - return wrappermap.wrapper(self).is_opaque() - - def drawRect_(self, rect): - context = drawing.DrawingContext(self, self.bounds(), rect) - context.style = drawing.DrawingStyle() - wrapper = wrappermap.wrapper(self) - wrapper.state = 'normal' - disabled = wrapper.get_disabled() - if not disabled: - if self.state() == NSOnState: - wrapper.state = 'pressed' - elif self.mouse_inside: - wrapper.state = 'hover' - else: - wrapper.state = 'normal' - - wrapper.draw(context, self.layout_manager) - self.layout_manager.reset() - - def sendAction_to_(self, action, to): - # We override the Cocoa machinery here and just send it to our wrapper - # widget. - wrapper = wrappermap.wrapper(self) - disabled = wrapper.get_disabled() - if not disabled: - wrapper.emit('clicked') - # Tell Cocoa we handled it anyway, just not emit the actual clicked - # event. - return YES -DrawableButton.setCellClass_(DrawableButtonCell) - -class ContinousButtonCell(DrawableButtonCell): - def stopTracking_at_inView_mouseIsUp_(self, lastPoint, at, view, mouseIsUp): - view.onStopTracking(at) - NSButtonCell.stopTracking_at_inView_mouseIsUp_(self, lastPoint, at, - view, mouseIsUp) - -class ContinuousDrawableButton(DrawableButton): - def init(self): - self = super(ContinuousDrawableButton, self).init() - self.setContinuous_(YES) - return self - - def mouseDown_(self, event): - self.releaseInbounds = self.stopTracking = self.firedOnce = False - self.cell().trackMouse_inRect_ofView_untilMouseUp_(event, - self.bounds(), self, YES) - wrapper = wrappermap.wrapper(self) - if not wrapper.get_disabled(): - if self.firedOnce: - wrapper.emit('released') - elif self.releaseInbounds: - wrapper.emit('clicked') - - def sendAction_to_(self, action, to): - if self.stopTracking: - return NO - self.firedOnce = True - wrapper = wrappermap.wrapper(self) - if not wrapper.get_disabled(): - wrapper.emit('held-down') - return YES - - def onStopTracking(self, mouseLocation): - self.releaseInbounds = NSPointInRect(mouseLocation, self.bounds()) - self.stopTracking = True -ContinuousDrawableButton.setCellClass_(ContinousButtonCell) - -class DragableButtonCell(NSButtonCell): - def startTrackingAt_inView_(self, point, view): - self.start_x = point.x - return YES - - def continueTracking_at_inView_(self, lastPoint, at, view): - DRAG_THRESHOLD = 15 - wrapper = wrappermap.wrapper(view) - if not wrapper.get_disabled(): - if (view.last_drag_event != 'right' and - at.x > self.start_x + DRAG_THRESHOLD): - wrapper.emit("dragged-right") - view.last_drag_event = 'right' - elif (view.last_drag_event != 'left' and - at.x < self.start_x - DRAG_THRESHOLD): - view.last_drag_event = 'left' - wrapper.emit("dragged-left") - return YES - -class DragableDrawableButton(DrawableButton): - def mouseDown_(self, event): - self.last_drag_event = None - self.cell().trackMouse_inRect_ofView_untilMouseUp_(event, - self.bounds(), self, YES) - - def sendAction_to_(self, action, to): - # only send the click event if we didn't send a - # dragged-left/dragged-right event - wrapper = wrappermap.wrapper(self) - if self.last_drag_event is None and not wrapper.get_disabled(): - wrapper.emit('clicked') - return YES -DragableDrawableButton.setCellClass_(DragableButtonCell) - -MouseTrackingInfo = collections.namedtuple("MouseTrackingInfo", - "start_pos click_pos") - -class CustomSliderCell(NSSliderCell): - def calc_slider_amount(self, view, pos, size): - slider_size = wrappermap.wrapper(view).slider_size() - pos -= slider_size / 2 - size -= slider_size - return max(0, min(1, float(pos) / size)) - - def get_slider_pos(self, view, value=None): - if value is None: - value = view.floatValue() - if view.isVertical(): - size = view.bounds().size.height - else: - size = view.bounds().size.width - slider_size = view.knobThickness() - size -= slider_size - start_pos = slider_size / 2.0 - ratio = ((value - view.minValue()) / - view.maxValue() - view.minValue()) - return start_pos + (ratio * size) - - def startTrackingAt_inView_(self, at, view): - wrapper = wrappermap.wrapper(view) - start_pos = self.get_slider_pos(view) - if self.isVertical(): - click_pos = at.y - else: - click_pos = at.x - # only move the cursor if the click was outside the slider - if abs(click_pos - start_pos) > view.knobThickness() / 2: - self.moveSliderTo(view, click_pos) - start_pos = click_pos - view.mouse_tracking_info = MouseTrackingInfo(start_pos, click_pos) - if not wrapper.get_disabled(): - wrapper.emit('pressed') - return YES - - def moveSliderTo(self, view, pos): - if view.isVertical(): - size = view.bounds().size.height - else: - size = view.bounds().size.width - - slider_amount = self.calc_slider_amount(view, pos, size) - value = (self.maxValue() - self.minValue()) * slider_amount - self.setFloatValue_(value) - wrapper = wrappermap.wrapper(view) - if not wrapper.get_disabled(): - wrapper.emit('moved', value) - if self.isContinuous(): - wrapper.emit('changed', value) - - def continueTracking_at_inView_(self, lastPoint, at, view): - if view.isVertical(): - mouse_pos = at.y - else: - mouse_pos = at.x - - info = view.mouse_tracking_info - new_pos = info.start_pos + (mouse_pos - info.click_pos) - self.moveSliderTo(view, new_pos) - return YES - - def stopTracking_at_inView_mouseIsUp_(self, lastPoint, at, view, mouseUp): - wrapper = wrappermap.wrapper(view) - if not wrapper.get_disabled(): - wrapper.emit('released') - view.mouse_tracking_info = None - -class CustomSliderView(NSSlider): - def init(self): - self = super(CustomSliderView, self).init() - self.layout_manager = LayoutManager() - self.custom_cursor = None - self.mouse_tracking_info = None - return self - - def get_slider_pos(self, value=None): - return self.cell().get_slider_pos(self, value) - - def resetCursorRects(self): - if self.custom_cursor is not None: - self.addCursorRect_cursor_(self.visibleRect(), self.custom_cursor) - self.custom_cursor.setOnMouseEntered_(YES) - - def isOpaque(self): - return wrappermap.wrapper(self).is_opaque() - - def knobThickness(self): - return wrappermap.wrapper(self).slider_size() - - def scrollWheel_(self, event): - wrapper = wrappermap.wrapper(self) - if wrapper.get_disabled(): - return - # NOTE: we ignore the scroll_step value passed into set_increments() - # and calculate the change using deltaY, which is in device - # coordinates. - slider_size = wrapper.slider_size() - if wrapper.is_horizontal(): - size = self.bounds().size.width - else: - size = self.bounds().size.height - size -= slider_size - - range = self.maxValue() - self.minValue() - value_change = (event.deltaY() / size) * range - self.setFloatValue_(self.floatValue() + value_change) - wrapper.emit('pressed') - wrapper.emit('changed', self.floatValue()) - wrapper.emit('released') - - def isVertical(self): - return not wrappermap.wrapper(self).is_horizontal() - - def drawRect_(self, rect): - context = drawing.DrawingContext(self, self.bounds(), rect) - context.style = drawing.DrawingStyle() - wrappermap.wrapper(self).draw(context, self.layout_manager) - self.layout_manager.reset() - - def sendAction_to_(self, action, to): - # We override the Cocoa machinery here and just send it to our wrapper - # widget. - wrapper = wrappermap.wrapper(self) - disabled = wrapper.get_disabled() - if not disabled: - wrapper.emit('changed', self.floatValue()) - # Total Cocoa we handled it anyway to prevent the event passed to - # upper layer. - return YES -CustomSliderView.setCellClass_(CustomSliderCell) - -class CustomControlBase(drawing.DrawingMixin, Widget): - def set_cursor(self, cursor): - if cursor == widgetconst.CURSOR_NORMAL: - self.view.custom_cursor = None - elif cursor == widgetconst.CURSOR_POINTING_HAND: - self.view.custom_cursor = NSCursor.pointingHandCursor() - else: - raise ValueError("Unknown cursor: %s" % cursor) - if self.view.window(): - self.view.window().invalidateCursorRectsForView_(self.view) - -class CustomButton(CustomControlBase): - """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class.""" - def __init__(self): - CustomControlBase.__init__(self) - self.create_signal('clicked') - self.view = DrawableButton.alloc().init() - self.view.setRefusesFirstResponder_(NO) - self.view.setEnabled_(True) - - def enable(self): - Widget.enable(self) - self.view.setNeedsDisplay_(YES) - - def disable(self): - Widget.disable(self) - self.view.setNeedsDisplay_(YES) - -class ContinuousCustomButton(CustomButton): - """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class.""" - def __init__(self): - CustomButton.__init__(self) - self.create_signal('held-down') - self.create_signal('released') - self.view = ContinuousDrawableButton.alloc().init() - self.view.setRefusesFirstResponder_(NO) - - def set_delays(self, initial, repeat): - self.view.cell().setPeriodicDelay_interval_(initial, repeat) - -class DragableCustomButton(CustomButton): - """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class.""" - def __init__(self): - CustomButton.__init__(self) - self.create_signal('dragged-left') - self.create_signal('dragged-right') - self.view = DragableDrawableButton.alloc().init() - -class CustomSlider(CustomControlBase): - """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class.""" - def __init__(self): - CustomControlBase.__init__(self) - self.create_signal('pressed') - self.create_signal('released') - self.create_signal('changed') - self.create_signal('moved') - self.view = CustomSliderView.alloc().init() - self.view.setRefusesFirstResponder_(NO) - if self.is_continuous(): - self.view.setContinuous_(YES) - else: - self.view.setContinuous_(NO) - self.view.setEnabled_(True) - - def get_slider_pos(self, value=None): - return self.view.get_slider_pos(value) - - def viewport_created(self): - self.view.cell().setKnobThickness_(self.slider_size()) - - def get_value(self): - return self.view.floatValue() - - def set_value(self, value): - self.view.setFloatValue_(value) - - def get_range(self): - return self.view.minValue(), self.view.maxValue() - - def set_range(self, min_value, max_value): - self.view.setMinValue_(min_value) - self.view.setMaxValue_(max_value) - - def set_increments(self, small_step, big_step, scroll_step=None): - # NOTE: we ignore all of these parameters. - # - # Cocoa doesn't have a concept of changing the increments for - # NSScroller. scroll_step is isn't really compatible with - # the event object that's passed to scrollWheel_() - pass - - def enable(self): - Widget.enable(self) - self.view.setNeedsDisplay_(YES) - - def disable(self): - Widget.disable(self) - self.view.setNeedsDisplay_(YES) diff --git a/mvc/widgets/osx/drawing.py b/mvc/widgets/osx/drawing.py deleted file mode 100644 index aaad1e9..0000000 --- a/mvc/widgets/osx/drawing.py +++ /dev/null @@ -1,289 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -"""miro.plat.frontend.widgets.drawing -- Draw on Views.""" - -import math - -from Foundation import * -from AppKit import * -#from Quartz import * -from objc import YES, NO, nil - - -class ImageSurface: - """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class.""" - def __init__(self, image): - """Create a new ImageSurface.""" - self.image = image.nsimage.copy() - self.width = image.width - self.height = image.height - - def get_size(self): - return self.width, self.height - - def draw(self, context, x, y, width, height, fraction=1.0): - if self.width == 0 or self.height == 0: - return - current_context = NSGraphicsContext.currentContext() - current_context.setShouldAntialias_(YES) - current_context.setImageInterpolation_(NSImageInterpolationHigh) - current_context.saveGraphicsState() - flip_context(y + height) - dest_rect = NSMakeRect(x, 0, width, height) - if self.width >= width and self.height >= height: - # drawing to area smaller than our image - source_rect = NSMakeRect(0, 0, width, height) - self.image.drawInRect_fromRect_operation_fraction_( - dest_rect, source_rect, NSCompositeSourceOver, fraction) - else: - # drawing to area larger than our image. Need to tile it. - NSColor.colorWithPatternImage_(self.image).set() - current_context.setPatternPhase_( - self._calc_pattern_phase(context, x, y)) - NSBezierPath.fillRect_(dest_rect) - current_context.restoreGraphicsState() - - def draw_rect(self, context, dest_x, dest_y, source_x, source_y, width, - height, fraction=1.0): - if width == 0 or height == 0: - return - current_context = NSGraphicsContext.currentContext() - current_context.setShouldAntialias_(YES) - current_context.setImageInterpolation_(NSImageInterpolationHigh) - current_context.saveGraphicsState() - flip_context(dest_y + height) - dest_y = 0 - dest_rect = NSMakeRect(dest_x, dest_y, width, height) - source_rect = NSMakeRect(source_x, self.height-source_y-height, - width, height) - self.image.drawInRect_fromRect_operation_fraction_( - dest_rect, source_rect, NSCompositeSourceOver, fraction) - current_context.restoreGraphicsState() - - def _calc_pattern_phase(self, context, x, y): - """Calculate the pattern phase to draw tiled images. - - When we draw with a pattern, we want the image in the pattern to start - at the top-left of where we're drawing to. This function does the - dirty work necessary. - - :returns: NSPoint to send to setPatternPhase_ - """ - # convert to view coords - view_point = NSPoint(context.origin.x + x, context.origin.y + y) - # convert to window coords, which is setPatternPhase_ uses - return context.view.convertPoint_toView_(view_point, nil) - -def convert_cocoa_color(color): - rgb = color.colorUsingColorSpaceName_(NSDeviceRGBColorSpace) - return (rgb.redComponent(), rgb.greenComponent(), rgb.blueComponent()) - -def convert_widget_color(color, alpha=1.0): - return NSColor.colorWithDeviceRed_green_blue_alpha_(color[0], color[1], - color[2], alpha) -def flip_context(height): - """Make the current context's coordinates flipped. - - This is useful for drawing images, since they use the normal cocoa - coordinates and we use flipped versions. - - :param height: height of the current area we are drawing to. - """ - xform = NSAffineTransform.transform() - xform.translateXBy_yBy_(0, height) - xform.scaleXBy_yBy_(1.0, -1.0) - xform.concat() - -class DrawingStyle(object): - """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class.""" - def __init__(self, bg_color=None, text_color=None): - self.use_custom_style = True - if text_color is None: - self.text_color = self.default_text_color - else: - self.text_color = convert_cocoa_color(text_color) - if bg_color is None: - self.bg_color = self.default_bg_color - else: - self.bg_color = convert_cocoa_color(bg_color) - - default_text_color = convert_cocoa_color(NSColor.textColor()) - default_bg_color = convert_cocoa_color(NSColor.textBackgroundColor()) - -class DrawingContext: - """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class.""" - def __init__(self, view, drawing_area, rect): - self.view = view - self.path = NSBezierPath.bezierPath() - self.color = NSColor.blackColor() - self.width = drawing_area.size.width - self.height = drawing_area.size.height - self.origin = drawing_area.origin - if drawing_area.origin != NSZeroPoint: - xform = NSAffineTransform.transform() - xform.translateXBy_yBy_(drawing_area.origin.x, - drawing_area.origin.y) - xform.concat() - - def move_to(self, x, y): - self.path.moveToPoint_(NSPoint(x, y)) - - def rel_move_to(self, dx, dy): - self.path.relativeMoveToPoint_(NSPoint(dx, dy)) - - def line_to(self, x, y): - self.path.lineToPoint_(NSPoint(x, y)) - - def rel_line_to(self, dx, dy): - self.path.relativeLineToPoint_(NSPoint(dx, dy)) - - def curve_to(self, x1, y1, x2, y2, x3, y3): - self.path.curveToPoint_controlPoint1_controlPoint2_( - NSPoint(x3, y3), NSPoint(x1, y1), NSPoint(x2, y2)) - - def rel_curve_to(self, dx1, dy1, dx2, dy2, dx3, dy3): - self.path.relativeCurveToPoint_controlPoint1_controlPoint2_( - NSPoint(dx3, dy3), NSPoint(dx1, dy1), NSPoint(dx2, dy2)) - - def arc(self, x, y, radius, angle1, angle2): - angle1 = (angle1 * 360) / (2 * math.pi) - angle2 = (angle2 * 360) / (2 * math.pi) - center = NSPoint(x, y) - self.path.appendBezierPathWithArcWithCenter_radius_startAngle_endAngle_(center, radius, angle1, angle2) - - def arc_negative(self, x, y, radius, angle1, angle2): - angle1 = (angle1 * 360) / (2 * math.pi) - angle2 = (angle2 * 360) / (2 * math.pi) - center = NSPoint(x, y) - self.path.appendBezierPathWithArcWithCenter_radius_startAngle_endAngle_clockwise_(center, radius, angle1, angle2, YES) - - def rectangle(self, x, y, width, height): - rect = NSMakeRect(x, y, width, height) - self.path.appendBezierPathWithRect_(rect) - - def set_color(self, color, alpha=1.0): - self.color = convert_widget_color(color, alpha) - self.color.set() - - def set_shadow(self, color, opacity, offset, blur_radius): - shadow = NSShadow.alloc().init() - # shadow offset is always in the cocoa coordinates, so we need to - # reverse the y part - shadow.setShadowOffset_(NSPoint(offset[0], -offset[1])) - shadow.setShadowBlurRadius_(blur_radius) - shadow.setShadowColor_(convert_widget_color(color, opacity)) - shadow.set() - - def set_line_width(self, width): - self.path.setLineWidth_(width) - - def stroke(self): - self.path.stroke() - self.path.removeAllPoints() - - def stroke_preserve(self): - self.path.stroke() - - def fill(self): - self.path.fill() - self.path.removeAllPoints() - - def fill_preserve(self): - self.path.fill() - - def clip(self): - self.path.addClip() - self.path.removeAllPoints() - - def save(self): - NSGraphicsContext.currentContext().saveGraphicsState() - - def restore(self): - NSGraphicsContext.currentContext().restoreGraphicsState() - - def gradient_fill(self, gradient): - self.gradient_fill_preserve(gradient) - self.path.removeAllPoints() - - def gradient_fill_preserve(self, gradient): - context = NSGraphicsContext.currentContext() - context.saveGraphicsState() - self.path.addClip() - gradient.draw() - context.restoreGraphicsState() - -class Gradient(object): - """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class.""" - def __init__(self, x1, y1, x2, y2): - self.x1, self.y1, self.x2, self.y2 = x1, y1, x2, y2 - self.start_color = None - self.end_color = None - - def set_start_color(self, (red, green, blue)): - self.start_color = (red, green, blue) - - def set_end_color(self, (red, green, blue)): - self.end_color = (red, green, blue) - - def draw(self): - start_color = convert_widget_color(self.start_color) - end_color = convert_widget_color(self.end_color) - nsgradient = NSGradient.alloc().initWithStartingColor_endingColor_(start_color, end_color) - start_point = NSPoint(self.x1, self.y1) - end_point = NSPoint(self.x2, self.y2) - nsgradient.drawFromPoint_toPoint_options_(start_point, end_point, 0) - -class DrawingMixin(object): - def calc_size_request(self): - return self.size_request(self.view.layout_manager) - - # squish width / squish height only make sense on GTK - def set_squish_width(self, setting): - pass - - def set_squish_height(self, setting): - pass - - # Default implementations for methods that subclasses override. - - def is_opaque(self): - return False - - def size_request(self, layout_manager): - return 0, 0 - - def draw(self, context, layout_manager): - pass - - def viewport_repositioned(self): - # since this is a Mixin class, we want to make sure that our other - # classes see the viewport_repositioned() call. - super(DrawingMixin, self).viewport_repositioned() - self.queue_redraw() diff --git a/mvc/widgets/osx/drawingwidgets.py b/mvc/widgets/osx/drawingwidgets.py deleted file mode 100644 index 74e8232..0000000 --- a/mvc/widgets/osx/drawingwidgets.py +++ /dev/null @@ -1,67 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -"""drawingviews.py -- views that support custom drawing.""" - -import wrappermap -import drawing -from .base import Widget, SimpleBin, FlippedView -from .layoutmanager import LayoutManager - -class DrawingView(FlippedView): - def init(self): - self = super(DrawingView, self).init() - self.layout_manager = LayoutManager() - return self - - def isOpaque(self): - return wrappermap.wrapper(self).is_opaque() - - def drawRect_(self, rect): - context = drawing.DrawingContext(self, self.bounds(), rect) - context.style = drawing.DrawingStyle() - wrappermap.wrapper(self).draw(context, self.layout_manager) - -class DrawingArea(drawing.DrawingMixin, Widget): - """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class.""" - def __init__(self): - Widget.__init__(self) - self.view = DrawingView.alloc().init() - -class Background(drawing.DrawingMixin, SimpleBin): - """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class.""" - def __init__(self): - SimpleBin.__init__(self) - self.view = DrawingView.alloc().init() - - def calc_size_request(self): - drawing_size = drawing.DrawingMixin.calc_size_request(self) - container_size = SimpleBin.calc_size_request(self) - return (max(container_size[0], drawing_size[0]), - max(container_size[1], drawing_size[1])) diff --git a/mvc/widgets/osx/fasttypes.c b/mvc/widgets/osx/fasttypes.c deleted file mode 100644 index 72d3b5b..0000000 --- a/mvc/widgets/osx/fasttypes.c +++ /dev/null @@ -1,540 +0,0 @@ -/* -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - */ - -#include <Python.h> - -/* - * fasttypes.c - * - * Datastructures written in C to be fast. This used to be a big C++ file - * that depended on boost. Nowadays we only define LinkedList, which is easy - * enough to implement in pure C. - */ - -static int nodes_deleted = 0; // debugging only - -/* forward define python type objects */ - -static PyTypeObject LinkedListType; -static PyTypeObject LinkedListIterType; - -/* Structure definitions */ - -typedef struct LinkedListNode { - PyObject *obj; - struct LinkedListNode* next; - struct LinkedListNode* prev; - int deleted; // Has this node been removed? - int iter_count; // How many LinkedListIters point to this node? -} LinkedListNode; - -typedef struct { - PyObject_HEAD - int count; - LinkedListNode* sentinal; - // sentinal object to make list operations simpler/faster and equivalent - // to the boost API. It's prev node is the last element in the list and - // it's next node is the first -} LinkedListObject; - -typedef struct { - PyObject_HEAD - LinkedListNode* node; - LinkedListObject* list; -} LinkedListIterObject; - -/* LinkedListNode */ - -void check_node_deleted(LinkedListNode* node) -{ - if(node->iter_count <= 0 && node->deleted) { - free(node); - nodes_deleted += 1; - } -} - -static int remove_node(LinkedListObject* self, LinkedListNode* node) -{ - if(node->obj == NULL) { - PyErr_SetString(PyExc_IndexError, "can't remove lastIter()"); - return 0; - } - node->next->prev = node->prev; - node->prev->next = node->next; - node->deleted = 1; - self->count -= 1; - Py_DECREF(node->obj); - check_node_deleted(node); - return 1; -} - -/* LinkedListIter */ - -void switch_node(LinkedListIterObject* self, LinkedListNode* new_node) -{ - LinkedListNode* old_node; - - old_node = self->node; - self->node = new_node; - old_node->iter_count--; - self->node->iter_count++; - check_node_deleted(old_node); -} - -// Note that we don't expose the new method to python. We create -// LinkedListIters in the factory methods firstIter() and lastIter() -static LinkedListIterObject* LinkedListIterObject_new(LinkedListObject*list, - LinkedListNode* node) -{ - LinkedListIterObject* self; - - self = (LinkedListIterObject*)PyType_GenericAlloc(&LinkedListIterType, 0); - if(self != NULL) { - self->node = node; - self->list = list; - node->iter_count++; - } - return self; -} - -static void LinkedListIterObject_dealloc(LinkedListIterObject* self) -{ - self->node->iter_count--; - check_node_deleted(self->node); -} - -static PyObject *LinkedListIter_forward(LinkedListIterObject* self, PyObject *obj) -{ - switch_node(self, self->node->next); - Py_RETURN_NONE; -} - -static PyObject *LinkedListIter_back(LinkedListIterObject* self, PyObject *obj) -{ - switch_node(self, self->node->prev); - Py_RETURN_NONE; -} - -static PyObject *LinkedListIter_value(LinkedListIterObject* self, PyObject *obj) -{ - PyObject* retval; - - if(self->node->deleted) { - PyErr_SetString(PyExc_ValueError, "Node deleted"); - return NULL; - } - retval = self->node->obj; - if(retval == NULL) { - PyErr_SetString(PyExc_IndexError, "can't get value of lastIter()"); - return NULL; - } - Py_INCREF(retval); - return retval; -} - -static PyObject *LinkedListIter_copy(LinkedListIterObject* self, PyObject *obj) -{ - return (PyObject*)LinkedListIterObject_new(self->list, self->node); -} - -static PyObject *LinkedListIter_valid(LinkedListIterObject* self, PyObject *obj) -{ - return PyBool_FromLong(self->node->deleted == 0); -} - -PyObject* LinkedListIter_richcmp(LinkedListIterObject *o1, - LinkedListIterObject *o2, int opid) -{ - if(!PyObject_TypeCheck(o1, &LinkedListIterType) || - !PyObject_TypeCheck(o2, &LinkedListIterType)) { - return Py_NotImplemented; - } - switch(opid) { - case Py_EQ: - if(o1->node == o2->node) Py_RETURN_TRUE; - else Py_RETURN_FALSE; - case Py_NE: - if(o1->node != o2->node) Py_RETURN_TRUE; - else Py_RETURN_FALSE; - default: - return Py_NotImplemented; - } -} - -static PyMethodDef LinkedListIter_methods[] = { - {"forward", (PyCFunction)LinkedListIter_forward, METH_NOARGS, - "Move to the next element", - }, - {"back", (PyCFunction)LinkedListIter_back, METH_NOARGS, - "Move to the previous element", - }, - {"value", (PyCFunction)LinkedListIter_value, METH_NOARGS, - "Return the current element", - }, - {"copy", (PyCFunction)LinkedListIter_copy, METH_NOARGS, - "Duplicate iter", - }, - {"valid", (PyCFunction)LinkedListIter_valid, METH_NOARGS, - "Test if the iter is valid", - }, - {NULL}, -}; - -static PyTypeObject LinkedListIterType = { - PyObject_HEAD_INIT(NULL) - 0, /* ob_size */ - "fasttypes.LinkedListIter", /* tp_name */ - sizeof(LinkedListIterObject), /* tp_basicsize */ - 0, /* tp_itemsize */ - (destructor)LinkedListIterObject_dealloc, /* tp_dealloc */ - 0, /* tp_print */ - 0, /* tp_getattr */ - 0, /* tp_setattr */ - 0, /* tp_compare */ - 0, /* tp_repr */ - 0, /* tp_as_number */ - 0, /* tp_as_sequence */ - 0, /* tp_as_mapping */ - 0, /* tp_hash */ - 0, /* tp_call */ - 0, /* tp_str */ - 0, /* tp_getattro */ - 0, /* tp_setattro */ - 0, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT|Py_TPFLAGS_HAVE_RICHCOMPARE, /* tp_flags */ - "fasttypes LinkedListIter", /* tp_doc */ - 0, /* tp_traverse */ - 0, /* tp_clear */ - (richcmpfunc)LinkedListIter_richcmp, /* tp_richcompare */ - 0, /* tp_weaklistoffset */ - 0, /* tp_iter */ - 0, /* tp_iternext */ - LinkedListIter_methods, /* tp_methods */ - 0, /* tp_members */ - 0, /* tp_getset */ - 0, /* tp_base */ - 0, /* tp_dict */ - 0, /* tp_descr_get */ - 0, /* tp_descr_set */ - 0, /* tp_dictoffset */ - 0, /* tp_init */ - 0, /* tp_alloc */ - 0, /* tp_new */ -}; - -/* LinkedList */ - -LinkedListNode* make_new_node(PyObject* obj, LinkedListNode* prev, - LinkedListNode* next) -{ - LinkedListNode* retval; - retval = malloc(sizeof(LinkedListNode)); - if(!retval) { - PyErr_SetString(PyExc_MemoryError, "can't create new node"); - return NULL; - } - Py_XINCREF(obj); - retval->obj = obj; - retval->prev = prev; - retval->next = next; - retval->iter_count = retval->deleted = 0; - return retval; -} - -void set_iter_type_error(PyObject* obj) -{ - // Set an exception when we expected a LinkedListIter and got something - // else - PyObject* args; - PyObject* fmt; - PyObject* err_str; - - args = Py_BuildValue("(O)", obj); - fmt = PyString_FromString("Expected LinkedListIter, got %r"); - err_str = PyString_Format(fmt, args); - PyErr_SetObject(PyExc_TypeError, err_str); - Py_DECREF(fmt); - Py_DECREF(err_str); - Py_DECREF(args); -} - -static PyObject* insert_before(LinkedListObject* self, LinkedListNode* node, - PyObject* obj) -{ - LinkedListNode* new_node; - PyObject* retval; - - new_node = make_new_node(obj, node->prev, node); - if(!new_node) return NULL; - node->prev->next = new_node; - node->prev = new_node; - self->count += 1; - retval = (PyObject*)LinkedListIterObject_new(self, new_node); - return retval; -} - -static PyObject* LinkedList_new(PyTypeObject *type, PyObject *args, PyObject *kwds) -{ - LinkedListObject *self; - LinkedListNode *sentinal; - - self = (LinkedListObject *)type->tp_alloc(type, 0); - if (self == NULL) return NULL; - - sentinal = make_new_node(NULL, NULL, NULL); - if(!sentinal) { - Py_DECREF(self); - return NULL; - } - self->sentinal = sentinal->next = sentinal->prev = sentinal; - sentinal->iter_count = 1; // prevent the sentinal from being deleted - self->count = 0; - - return (PyObject *)self; -} - -static void LinkedList_dealloc(LinkedListObject* self) -{ - LinkedListNode *node, *tmp; - - node = self->sentinal->next; - while(node != self->sentinal) { - node->deleted = 1; - tmp = node->next; - check_node_deleted(node); - node = tmp; - } - - self->sentinal->iter_count -= 1; - check_node_deleted(self->sentinal); - return; -} - -static int LinkedList_init(LinkedListObject *self) -{ - self->count = 0; - return 0; -} - -static Py_ssize_t LinkedList_len(LinkedListObject *self) -{ - return self->count; -} - -static PyObject* LinkedList_get(LinkedListObject *self, - LinkedListIterObject *iter) -{ - if(!PyObject_TypeCheck(iter, &LinkedListIterType)) { - set_iter_type_error((PyObject*)iter); - return NULL; - } - return PyObject_CallMethod((PyObject*)iter, "value", "()"); -} -int LinkedList_set(LinkedListObject *self, LinkedListIterObject *iter, - PyObject *value) -{ - if(!PyObject_TypeCheck(iter, &LinkedListIterType)) { - set_iter_type_error((PyObject*)iter); - return -1; - } - if(iter->node->deleted) { - PyErr_SetString(PyExc_ValueError, "Node deleted"); - return -1; - } - if(iter->node->obj == NULL) { - PyErr_SetString(PyExc_IndexError, "can't set value of lastIter()"); - return -1; - } - if(value == NULL) { - if(!remove_node(self, iter->node)) return -1; - return 0; - } - Py_INCREF(value); - Py_DECREF(iter->node->obj); - iter->node->obj = value; - return 0; -} - -static PyObject *LinkedList_insertBefore(LinkedListObject* self, PyObject *args) -{ - LinkedListIterObject *iter; - PyObject *obj; - - if(!PyArg_ParseTuple(args, "OO", &iter, &obj)) return NULL; - if(!PyObject_TypeCheck(iter, &LinkedListIterType)) { - set_iter_type_error(obj); - return NULL; - } - - return insert_before(self, iter->node, obj); -} - -static PyObject *LinkedList_append(LinkedListObject* self, PyObject *obj) -{ - return insert_before(self, self->sentinal, obj); -} - -static PyObject *LinkedList_remove(LinkedListObject* self, - LinkedListIterObject *iter) -{ - LinkedListNode* next_node; - if(!PyObject_TypeCheck(iter, &LinkedListIterType)) { - set_iter_type_error((PyObject*)iter); - return NULL; - } - - next_node = iter->node->next; - if(!remove_node(self, iter->node)) return NULL; - return (PyObject*)LinkedListIterObject_new(self, next_node); -} - -static PyObject *LinkedList_firstIter(LinkedListObject* self, PyObject *obj) -{ - PyObject* retval; - retval = (PyObject*)LinkedListIterObject_new(self, self->sentinal->next); - return retval; -} - -static PyObject *LinkedList_lastIter(LinkedListObject* self, PyObject *obj) -{ - PyObject* retval; - retval = (PyObject*)LinkedListIterObject_new(self, self->sentinal); - return retval; -} - -static PyMappingMethods LinkedListMappingMethods = { - (lenfunc)LinkedList_len, - (binaryfunc)LinkedList_get, - (objobjargproc)LinkedList_set, -}; - -static PyMethodDef LinkedList_methods[] = { - {"insertBefore", (PyCFunction)LinkedList_insertBefore, METH_VARARGS, - "insert an element before iter", - }, - {"append", (PyCFunction)LinkedList_append, METH_O, - "append an element to the list", - }, - {"remove", (PyCFunction)LinkedList_remove, METH_O, - "remove an element to the list", - }, - {"firstIter", (PyCFunction)LinkedList_firstIter, METH_NOARGS, - "get an iter pointing to the first element in the list", - }, - {"lastIter", (PyCFunction)LinkedList_lastIter, METH_NOARGS, - "get an iter pointing to the last element in the list", - }, - {NULL}, -}; - -static PyTypeObject LinkedListType = { - PyObject_HEAD_INIT(NULL) - 0, /* ob_size */ - "fasttypes.LinkedList", /* tp_name */ - sizeof(LinkedListObject), /* tp_basicsize */ - 0, /* tp_itemsize */ - (destructor)LinkedList_dealloc, /* tp_dealloc */ - 0, /* tp_print */ - 0, /* tp_getattr */ - 0, /* tp_setattr */ - 0, /* tp_compare */ - 0, /* tp_repr */ - 0, /* tp_as_number */ - 0, /* tp_as_sequence */ - &LinkedListMappingMethods, /* tp_as_mapping */ - 0, /* tp_hash */ - 0, /* tp_call */ - 0, /* tp_str */ - 0, /* tp_getattro */ - 0, /* tp_setattro */ - 0, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT, /* tp_flags */ - "fasttypes LinkedList", /* tp_doc */ - 0, /* tp_traverse */ - 0, /* tp_clear */ - 0, /* tp_richcompare */ - 0, /* tp_weaklistoffset */ - 0, /* tp_iter */ - 0, /* tp_iternext */ - LinkedList_methods, /* tp_methods */ - 0, /* tp_members */ - 0, /* tp_getset */ - 0, /* tp_base */ - 0, /* tp_dict */ - 0, /* tp_descr_get */ - 0, /* tp_descr_set */ - 0, /* tp_dictoffset */ - (initproc)LinkedList_init, /* tp_init */ - 0, /* tp_alloc */ - LinkedList_new, /* tp_new */ -}; - -/* Module-level stuff */ - -static PyObject *count_nodes_deleted(PyObject *obj) -{ - return PyInt_FromLong(nodes_deleted); -} - -static PyObject *reset_nodes_deleted(PyObject *obj) -{ - nodes_deleted = 0; - Py_RETURN_NONE; -} - - -static PyMethodDef FasttypesMethods[] = -{ - {"_count_nodes_deleted", (PyCFunction)count_nodes_deleted, METH_NOARGS, - "get a count of how many nodes have been deleted (DEBUGGING ONLY)", - }, - {"_reset_nodes_deleted", (PyCFunction)reset_nodes_deleted, METH_NOARGS, - "reset the count of how many nodes have been deleted (DEBUGGING ONLY)", - }, - { NULL, NULL, 0, NULL } -}; - -PyMODINIT_FUNC initfasttypes(void) -{ - PyObject *m; - - if (PyType_Ready(&LinkedListType) < 0) - return; - - if (PyType_Ready(&LinkedListIterType) < 0) - return; - - m = Py_InitModule("fasttypes", FasttypesMethods); - - Py_INCREF(&LinkedListType); - Py_INCREF(&LinkedListIterType); - PyModule_AddObject(m, "LinkedList", (PyObject *)&LinkedListType); -} diff --git a/mvc/widgets/osx/helpers.py b/mvc/widgets/osx/helpers.py deleted file mode 100644 index e4aa23a..0000000 --- a/mvc/widgets/osx/helpers.py +++ /dev/null @@ -1,95 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -"""helper classes.""" - -import logging -import traceback - -from Foundation import * -from objc import nil - -class NotificationForwarder(NSObject): - """Forward notifications from a Cocoa object to a python class. - """ - - def initWithNSObject_center_(self, nsobject, center): - """Initialize the NotificationForwarder nsobject is the NSObject to - forward notifications for. It can be nil in which case notifications - from all objects will be forwarded. - - center is the NSNotificationCenter to get notifications from. It can - be None, in which cas the default notification center is used. - """ - self.nsobject = nsobject - self.callback_map = {} - if center is None: - self.center = NSNotificationCenter.defaultCenter() - else: - self.center = center - return self - - @classmethod - def create(cls, object, center=None): - """Helper method to call aloc() then initWithNSObject_center_().""" - return cls.alloc().initWithNSObject_center_(object, center) - - def connect(self, callback, name): - """Register to listen for notifications. - Only one callback for each notification name can be connected. - """ - - if name in self.callback_map: - raise ValueError("%s already connected" % name) - - self.callback_map[name] = callback - self.center.addObserver_selector_name_object_(self, 'observe:', name, - self.nsobject) - - def disconnect(self, name=None): - if name is not None: - self.center.removeObserver_name_object_(self, name, self.nsobject) - self.callback_map.pop(name) - else: - self.center.removeObserver_(self) - self.callback_map.clear() - - def observe_(self, notification): - name = notification.name() - callback = self.callback_map[name] - if callback is None: - logging.warn("Callback for %s is dead", name) - self.center.removeObverser_name_object_(self, name, self.nsobject) - return - try: - callback(notification) - except: - logging.warn("Callback for %s raised exception:%s\n", - name.encode('utf-8'), - traceback.format_exc()) diff --git a/mvc/widgets/osx/layout.py b/mvc/widgets/osx/layout.py deleted file mode 100644 index 0238975..0000000 --- a/mvc/widgets/osx/layout.py +++ /dev/null @@ -1,748 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -""".layout -- Widgets that handle laying out other -widgets. - -We basically follow GTK's packing model. Widgets are packed into vboxes, -hboxes or other container widgets. The child widgets request a minimum size, -and the container widgets allocate space for their children. Widgets may get -more size then they requested in which case they have to deal with it. In -rare cases, widgets may get less size then they requested in which case they -should just make sure they don't throw an exception or segfault. - -Check out the GTK tutorial for more info. -""" - -import itertools - -from AppKit import * -from Foundation import * -from objc import YES, NO, nil, signature, loadBundle - -import tableview -import wrappermap -from .base import Container, Bin, FlippedView -from mvc.utils import Matrix - -# These don't seem to be in pyobjc's AppKit (yet) -NSScrollerKnobStyleDefault = 0 -NSScrollerKnobStyleDark = 1 -NSScrollerKnobStyleLight = 2 - -NSScrollerStyleLegacy = 0 -NSScrollerStyleOverlay = 1 - -def _extra_space_iter(extra_length, count): - """Utility function to allocate extra space left over in containers.""" - if count == 0: - return - extra_space, leftover = divmod(extra_length, count) - while leftover >= 1: - yield extra_space + 1 - leftover -= 1 - yield extra_space + leftover - while True: - yield extra_space - -class BoxPacking: - """Utility class to store how we are packing a single widget.""" - - def __init__(self, widget, expand, padding): - self.widget = widget - self.expand = expand - self.padding = padding - -class Box(Container): - """Base class for HBox and VBox. """ - CREATES_VIEW = False - - def __init__(self, spacing=0): - self.spacing = spacing - Container.__init__(self) - self.packing_start = [] - self.packing_end = [] - self.expand_count = 0 - - def packing_both(self): - return itertools.chain(self.packing_start, self.packing_end) - - def get_children(self): - for packing in self.packing_both(): - yield packing.widget - children = property(get_children) - - # Internally Boxes use a (length, breadth) coordinate system. length and - # breadth will be either x or y depending on which way the box is - # oriented. The subclasses must provide methods to translate between the - # 2 coordinate systems. - - def translate_size(self, size): - """Translate a (width, height) tulple to (length, breadth).""" - raise NotImplementedError() - - def untranslate_size(self, size): - """Reverse the work of translate_size.""" - raise NotImplementedError() - - def make_child_rect(self, position, length): - """Create a rect to position a child with.""" - raise NotImplementedError() - - def pack_start(self, child, expand=False, padding=0): - self.packing_start.append(BoxPacking(child, expand, padding)) - if expand: - self.expand_count += 1 - self.child_added(child) - - def pack_end(self, child, expand=False, padding=0): - self.packing_end.append(BoxPacking(child, expand, padding)) - if expand: - self.expand_count += 1 - self.child_added(child) - - def _remove_from_packing(self, child): - for i in xrange(len(self.packing_start)): - if self.packing_start[i].widget is child: - return self.packing_start.pop(i) - for i in xrange(len(self.packing_end)): - if self.packing_end[i].widget is child: - return self.packing_end.pop(i) - raise LookupError("%s not found" % child) - - def remove(self, child): - packing = self._remove_from_packing(child) - if packing.expand: - self.expand_count -= 1 - self.child_removed(child) - - def translate_widget_size(self, widget): - return self.translate_size(widget.get_size_request()) - - def calc_size_request(self): - length = breadth = 0 - for packing in self.packing_both(): - child_length, child_breadth = \ - self.translate_widget_size(packing.widget) - length += child_length - if packing.padding: - length += packing.padding * 2 # Need to pad on both sides - breadth = max(breadth, child_breadth) - spaces = max(0, len(self.packing_start) + len(self.packing_end) - 1) - length += spaces * self.spacing - return self.untranslate_size((length, breadth)) - - def place_children(self): - request_length, request_breadth = self.translate_widget_size(self) - ps = self.viewport.placement.size - total_length, dummy = self.translate_size((ps.width, ps.height)) - total_extra_space = total_length - request_length - extra_space_iter = _extra_space_iter(total_extra_space, - self.expand_count) - start_end = self._place_packing_list(self.packing_start, - extra_space_iter, 0) - if self.expand_count == 0 and total_extra_space > 0: - # account for empty space after the end of pack_start list and - # before the pack_end list. - self.draw_empty_space(start_end, total_extra_space) - start_end += total_extra_space - self._place_packing_list(reversed(self.packing_end), extra_space_iter, - start_end) - - def draw_empty_space(self, start, length): - empty_rect = self.make_child_rect(start, length) - my_view = self.viewport.view - opaque_view = my_view.opaqueAncestor() - if opaque_view is not None: - empty_rect2 = opaque_view.convertRect_fromView_(empty_rect, my_view) - opaque_view.setNeedsDisplayInRect_(empty_rect2) - - def _place_packing_list(self, packing_list, extra_space_iter, position): - for packing in packing_list: - child_length, child_breadth = \ - self.translate_widget_size(packing.widget) - if packing.expand: - child_length += extra_space_iter.next() - if packing.padding: # space before - self.draw_empty_space(position, packing.padding) - position += packing.padding - child_rect = self.make_child_rect(position, child_length) - if packing.padding: # space after - self.draw_empty_space(position, packing.padding) - position += packing.padding - packing.widget.place(child_rect, self.viewport.view) - position += child_length - if self.spacing > 0: - self.draw_empty_space(position, self.spacing) - position += self.spacing - return position - - def enable(self): - Container.enable(self) - for mem in self.children: - mem.enable() - - def disable(self): - Container.disable(self) - for mem in self.children: - mem.disable() - -class VBox(Box): - """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class.""" - def translate_size(self, size): - return (size[1], size[0]) - - def untranslate_size(self, size): - return (size[1], size[0]) - - def make_child_rect(self, position, length): - placement = self.viewport.placement - return NSMakeRect(placement.origin.x, placement.origin.y + position, - placement.size.width, length) - -class HBox(Box): - """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class.""" - def translate_size(self, size): - return (size[0], size[1]) - - def untranslate_size(self, size): - return (size[0], size[1]) - - def make_child_rect(self, position, length): - placement = self.viewport.placement - return NSMakeRect(placement.origin.x + position, placement.origin.y, - length, placement.size.height) - -class Alignment(Bin): - """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class.""" - CREATES_VIEW = False - - def __init__(self, xalign=0.0, yalign=0.0, xscale=0.0, yscale=0.0, - top_pad=0, bottom_pad=0, left_pad=0, right_pad=0): - Bin.__init__(self) - self.xalign = xalign - self.yalign = yalign - self.xscale = xscale - self.yscale = yscale - self.top_pad = top_pad - self.bottom_pad = bottom_pad - self.left_pad = left_pad - self.right_pad = right_pad - if self.child is not None: - self.place_children() - - def set(self, xalign=0.0, yalign=0.0, xscale=0.0, yscale=0.0): - self.xalign = xalign - self.yalign = yalign - self.xscale = xscale - self.yscale = yscale - if self.child is not None: - self.place_children() - - def set_padding(self, top_pad=0, bottom_pad=0, left_pad=0, right_pad=0): - self.top_pad = top_pad - self.bottom_pad = bottom_pad - self.left_pad = left_pad - self.right_pad = right_pad - if self.child is not None and self.viewport is not None: - self.place_children() - - def vertical_pad(self): - return self.top_pad + self.bottom_pad - - def horizontal_pad(self): - return self.left_pad + self.right_pad - - def calc_size_request(self): - if self.child: - child_width, child_height = self.child.get_size_request() - return (child_width + self.horizontal_pad(), - child_height + self.vertical_pad()) - else: - return (0, 0) - - def calc_size(self, requested, total, scale): - extra_width = max(0, total - requested) - return requested + int(round(extra_width * scale)) - - def calc_position(self, size, total, align): - return int(round((total - size) * align)) - - def place_children(self): - if self.child is None: - return - - total_width = self.viewport.placement.size.width - total_height = self.viewport.placement.size.height - total_width -= self.horizontal_pad() - total_height -= self.vertical_pad() - request_width, request_height = self.child.get_size_request() - - child_width = self.calc_size(request_width, total_width, self.xscale) - child_height = self.calc_size(request_height, total_height, self.yscale) - child_x = self.calc_position(child_width, total_width, self.xalign) - child_y = self.calc_position(child_height, total_height, self.yalign) - child_x += self.left_pad - child_y += self.top_pad - - my_origin = self.viewport.area().origin - child_rect = NSMakeRect(my_origin.x + child_x, my_origin.y + child_y, child_width, child_height) - self.child.place(child_rect, self.viewport.view) - # Make sure the space not taken up by our child is redrawn. - self.viewport.queue_redraw() - -class DetachedWindowHolder(Alignment): - def __init__(self): - Alignment.__init__(self, bottom_pad=16, xscale=1.0, yscale=1.0) - -class _TablePacking(object): - """Utility class to help with packing Table widgets.""" - def __init__(self, widget, column, row, column_span, row_span): - self.widget = widget - self.column = column - self.row = row - self.column_span = column_span - self.row_span = row_span - - def column_indexes(self): - return range(self.column, self.column + self.column_span) - - def row_indexes(self): - return range(self.row, self.row + self.row_span) - -class Table(Container): - """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class.""" - CREATES_VIEW = False - - def __init__(self, columns, rows): - Container.__init__(self) - self._cells = Matrix(columns, rows) - self._children = [] # List of _TablePacking objects - self._children_sorted = True - self.rows = rows - self.columns = columns - self.row_spacing = self.column_spacing = 0 - - def _ensure_children_sorted(self): - if not self._children_sorted: - def cell_area(table_packing): - return table_packing.column_span * table_packing.row_span - self._children.sort(key=cell_area) - self._children_sorted = True - - def get_children(self): - return [cell.widget for cell in self._children] - children = property(get_children) - - def calc_size_request(self): - self._ensure_children_sorted() - self._calc_dimensions() - return self.total_width, self.total_height - - def _calc_dimensions(self): - self.column_widths = [0] * self.columns - self.row_heights = [0] * self.rows - - for tp in self._children: - child_width, child_height = tp.widget.get_size_request() - # recalc the width of the child's columns - self._recalc_dimension(child_width, self.column_widths, - tp.column_indexes()) - # recalc the height of the child's rows - self._recalc_dimension(child_height, self.row_heights, - tp.row_indexes()) - - self.total_width = (self.column_spacing * (self.columns - 1) + - sum(self.column_widths)) - self.total_height = (self.row_spacing * (self.rows - 1) + - sum(self.row_heights)) - - def _recalc_dimension(self, child_size, size_array, positions): - current_size = sum(size_array[p] for p in positions) - child_size_needed = child_size - current_size - if child_size_needed > 0: - iter = _extra_space_iter(child_size_needed, len(positions)) - for p in positions: - size_array[p] += iter.next() - - def place_children(self): - # This method depepnds on us calling _calc_dimensions() in - # calc_size_request(). Ensure that this happens. - if self.cached_size_request is None: - self.get_size_request() - column_positions = [0] - for width in self.column_widths[:-1]: - column_positions.append(width + column_positions[-1] + self.column_spacing) - row_positions = [0] - for height in self.row_heights[:-1]: - row_positions.append(height + row_positions[-1] + self.row_spacing) - - my_x= self.viewport.placement.origin.x - my_y = self.viewport.placement.origin.y - for tp in self._children: - x = my_x + column_positions[tp.column] - y = my_y + row_positions[tp.row] - width = sum(self.column_widths[i] for i in tp.column_indexes()) - height = sum(self.row_heights[i] for i in tp.row_indexes()) - rect = NSMakeRect(x, y, width, height) - tp.widget.place(rect, self.viewport.view) - - def pack(self, widget, column, row, column_span=1, row_span=1): - tp = _TablePacking(widget, column, row, column_span, row_span) - for c in tp.column_indexes(): - for r in tp.row_indexes(): - if self._cells[c, r]: - raise ValueError("Cell %d x %d is already taken" % (c, r)) - self._cells[column, row] = widget - self._children.append(tp) - self._children_sorted = False - self.child_added(widget) - - def remove(self, child): - for i in xrange(len(self._children)): - if self._children[i].widget is child: - self._children.remove(i) - break - else: - raise ValueError("%s is not a child of this Table" % child) - self._cells.remove(child) - self.child_removed(widget) - - def set_column_spacing(self, spacing): - self.column_spacing = spacing - self.invalidate_size_request() - - def set_row_spacing(self, spacing): - self.row_spacing = spacing - self.invalidate_size_request() - - def enable(self, row=None, column=None): - Container.enable(self) - if row != None and column != None: - if self._cells[column, row]: - self._cells[column, row].enable() - elif row != None: - for mem in self._cells.row(row): - if mem: mem.enable() - elif column != None: - for mem in self._cells.column(column): - if mem: mem.enable() - else: - for mem in self._cells: - if mem: mem.enable() - - def disable(self, row=None, column=None): - Container.disable(self) - if row != None and column != None: - if self._cells[column, row]: - self._cells[column, row].disable() - elif row != None: - for mem in self._cells.row(row): - if mem: mem.disable() - elif column != None: - for mem in self._cells.column(column): - if mem: mem.disable() - else: - for mem in self._cells: - if mem: mem.disable() - -class MiroScrollView(NSScrollView): - def tile(self): - NSScrollView.tile(self) - # tile is called when we need to layout our child view and scrollers. - # This probably means that we've either hidden or shown a scrollbar so - # call invalidate_size_request to ensure that things get re-layed out - # correctly. (#see 13842) - wrapper = wrappermap.wrapper(self) - if wrapper is not None: - wrapper.invalidate_size_request() - -class Scroller(Bin): - """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class.""" - def __init__(self, horizontal, vertical): - Bin.__init__(self) - self.view = MiroScrollView.alloc().init() - self.view.setAutohidesScrollers_(YES) - self.view.setHasHorizontalScroller_(horizontal) - self.view.setHasVerticalScroller_(vertical) - self.document_view = FlippedView.alloc().init() - self.view.setDocumentView_(self.document_view) - - def prepare_for_dark_content(self): - try: - self.view.setScrollerKnobStyle_(NSScrollerKnobStyleLight) - except AttributeError: - # This only works on 10.7 and abvoe - pass - - def set_has_borders(self, has_border): - self.view.setBorderType_(NSBezelBorder) - - def viewport_repositioned(self): - # If the window is resized, this translates to a - # viewport_repositioned() event. Instead of calling - # place_children() one, which is what our suporclass does, we need - # some extra logic here. place the chilren to work out if we need a - # scrollbar, then get the new size, then replace the children (which - # now takes into account of scrollbar size.) - super(Scroller, self).viewport_repositioned() - self.cached_size_request = self.calc_size_request() - self.place_children() - - def set_background_color(self, color): - self.view.setBackgroundColor_(self.make_color(color)) - - def add(self, child): - child.parent_is_scroller = True - Bin.add(self, child) - - def remove(self): - child.parent_is_scroller = False - Bin.remove(self) - - def children_changed(self): - # since our size isn't dependent on our children, don't call - # invalidate_size_request() here. Just call place_children() so that - # they get positioned correctly in the document view. - # - # XXX dodgy - why are we laying out the children twice? When the - # children change, the scroller could appear/disappear. But you have - # no idea if that's going to happen without knowing how big your - # children are. So we lay it out, get the size, then, place the - # children again. This makes sure that the right side of the children - # are redrawn. There's got to be a better way?? - self.place_children() - self.cached_size_request = self.calc_size_request() - self.place_children() - - def calc_size_request(self): - if self.child: - width = height = 0 - try: - legacy = self.view.scrollerStyle() == NSScrollerStyleLegacy - except AttributeError: - legacy = True - if not self.view.hasHorizontalScroller(): - width = self.child.get_size_request()[0] - if not self.view.hasVerticalScroller(): - height = self.child.get_size_request()[1] - # Add a little room for the scrollbars (if necessary) - if legacy and self.view.hasHorizontalScroller(): - height += NSScroller.scrollerWidth() - if legacy and self.view.hasVerticalScroller(): - width += NSScroller.scrollerWidth() - return width, height - else: - return 0, 0 - - def place_children(self): - if self.child is not None: - scroll_view_size = self.view.contentView().frame().size - child_width, child_height = self.child.get_size_request() - child_width = max(child_width, scroll_view_size.width) - child_height = max(child_height, scroll_view_size.height) - frame = NSRect(NSPoint(0,0), NSSize(child_width, child_height)) - if isinstance(self.child, tableview.TableView) and self.child.is_showing_headers(): - # Hack to allow the content of a table view to scroll, but not - # the headers - self.child.place(frame, self.document_view) - if self.view.documentView() is not self.child.tableview: - self.view.setDocumentView_(self.child.tableview) - else: - self.child.place(frame, self.document_view) - self.document_view.setFrame_(frame) - self.document_view.setNeedsDisplay_(YES) - self.view.setNeedsDisplay_(YES) - self.child.emit('place-in-scroller') - -class ExpanderView(FlippedView): - def init(self): - self = super(ExpanderView, self).init() - self.label_rect = None - self.content_view = None - self.button = NSButton.alloc().init() - self.button.setState_(NSOffState) - self.button.setTitle_("") - self.button.setBezelStyle_(NSDisclosureBezelStyle) - self.button.setButtonType_(NSPushOnPushOffButton) - self.button.sizeToFit() - self.addSubview_(self.button) - self.button.setTarget_(self) - self.button.setAction_('buttonChanged:') - self.content_view = FlippedView.alloc().init() - return self - - def buttonChanged_(self, button): - if button.state() == NSOnState: - self.addSubview_(self.content_view) - else: - self.content_view.removeFromSuperview() - if self.window(): - wrappermap.wrapper(self).invalidate_size_request() - - def mouseDown_(self, event): - pass # Just need to respond to the selector so we get mouseUp_ - - def mouseUp_(self, event): - position = event.locationInWindow() - window_label_rect = self.convertRect_toView_(self.label_rect, None) - if NSPointInRect(position, window_label_rect): - self.button.setNextState() - self.buttonChanged_(self.button) - -class Expander(Bin): - BUTTON_PAD_TOP = 2 - BUTTON_PAD_LEFT = 4 - LABEL_SPACING = 4 - - def __init__(self, child): - Bin.__init__(self) - if child: - self.add(child) - self.label = None - self.spacing = 0 - self.view = ExpanderView.alloc().init() - self.button = self.view.button - self.button.setFrameOrigin_(NSPoint(self.BUTTON_PAD_LEFT, - self.BUTTON_PAD_TOP)) - self.content_view = self.view.content_view - - def remove_viewport(self): - Bin.remove_viewport(self) - if self.label is not None: - self.label.remove_viewport() - - def set_spacing(self, spacing): - self.spacing = spacing - - def set_label(self, widget): - if self.label is not None: - self.label.remove_viewport() - self.label = widget - self.children_changed() - - def set_expanded(self, expanded): - if expanded: - self.button.setState_(NSOnState) - else: - self.button.setState_(NSOffState) - self.view.buttonChanged_(self.button) - - def calc_top_size(self): - width = self.button.bounds().size.width - height = self.button.bounds().size.height - if self.label is not None: - label_width, label_height = self.label.get_size_request() - width += self.LABEL_SPACING + label_width - height = max(height, label_height) - width += self.BUTTON_PAD_LEFT - height += self.BUTTON_PAD_TOP - return width, height - - def calc_size_request(self): - width, height = self.calc_top_size() - if self.child is not None and self.button.state() == NSOnState: - child_width, child_height = self.child.get_size_request() - width = max(width, child_width) - height += self.spacing + child_height - return width, height - - def place_children(self): - top_width, top_height = self.calc_top_size() - if self.label: - label_width, label_height = self.label.get_size_request() - button_width = self.button.bounds().size.width - label_x = self.BUTTON_PAD_LEFT + button_width + self.LABEL_SPACING - label_rect = NSMakeRect(label_x, self.BUTTON_PAD_TOP, - label_width, label_height) - self.label.place(label_rect, self.viewport.view) - self.view.label_rect = label_rect - if self.child: - size = self.viewport.area().size - child_rect = NSMakeRect(0, 0, size.width, size.height - - top_height) - self.content_view.setFrame_(NSMakeRect(0, top_height, size.width, - size.height - top_height)) - self.child.place(child_rect, self.content_view) - - -class TabViewDelegate(NSObject): - def tabView_willSelectTabViewItem_(self, tab_view, tab_view_item): - try: - wrapper = wrappermap.wrapper(tab_view) - except KeyError: - pass # The NSTabView hasn't been placed yet, don't worry about it. - else: - wrapper.place_child_with_item(tab_view_item) - -class TabContainer(Container): - def __init__(self): - Container.__init__(self) - self.children = [] - self.item_to_child = {} - self.view = NSTabView.alloc().init() - self.view.setAllowsTruncatedLabels_(NO) - self.delegate = TabViewDelegate.alloc().init() - self.view.setDelegate_(self.delegate) - - def append_tab(self, child_widget, label, image): - item = NSTabViewItem.alloc().init() - item.setLabel_(label) - item.setView_(FlippedView.alloc().init()) - self.view.addTabViewItem_(item) - self.children.append(child_widget) - self.child_added(child_widget) - self.item_to_child[item] = child_widget - - def select_tab(self, index): - self.view.selectTabViewItemAtIndex_(index) - - def place_children(self): - self.place_child_with_item(self.view.selectedTabViewItem()) - - def place_child_with_item(self, tab_view_item): - child = self.item_to_child[tab_view_item] - child_view = tab_view_item.view() - content_rect =self.view.contentRect() - child_view.setFrame_(content_rect) - child.place(child_view.bounds(), child_view) - - def calc_size_request(self): - tab_size = self.view.minimumSize() - # make sure there's enough room for the tabs, plus a little extra - # space to make things look good - max_width = tab_size.width + 60 - max_height = 0 - for child in self.children: - width, height = child.get_size_request() - max_width = max(width, max_width) - max_height = max(height, max_height) - max_height += tab_size.height - - return max_width, max_height diff --git a/mvc/widgets/osx/layoutmanager.py b/mvc/widgets/osx/layoutmanager.py deleted file mode 100644 index de4301b..0000000 --- a/mvc/widgets/osx/layoutmanager.py +++ /dev/null @@ -1,445 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -"""textlayout.py -- Contains the LayoutManager class. It handles laying text, -buttons, getting font metrics and other tasks that are required to size -things. -""" -import logging -import math - -from AppKit import * -from Foundation import * -from objc import YES, NO, nil - -import drawing - -INFINITE = 1000000 # size of an "infinite" dimension - -class MiroLayoutManager(NSLayoutManager): - """Overide NSLayoutManager to draw better underlines.""" - - def drawUnderlineForGlyphRange_underlineType_baselineOffset_lineFragmentRect_lineFragmentGlyphRange_containerOrigin_(self, glyph_range, typ, offset, line_rect, line_glyph_range, container_origin): - container, _ = self.textContainerForGlyphAtIndex_effectiveRange_(glyph_range.location, None) - rect = self.boundingRectForGlyphRange_inTextContainer_(glyph_range, container) - x = container_origin.x + rect.origin.x - y = (container_origin.y + rect.origin.y + rect.size.height - offset) - underline_height, offset = self.calc_underline_extents(glyph_range) - y = math.ceil(y + offset) + underline_height / 2.0 - path = NSBezierPath.bezierPath() - path.setLineWidth_(underline_height) - path.moveToPoint_(NSPoint(x, y)) - path.relativeLineToPoint_(NSPoint(rect.size.width, 0)) - path.stroke() - - def calc_underline_extents(self, line_glyph_range): - index = self.characterIndexForGlyphAtIndex_(line_glyph_range.location) - font, _ = self.textStorage().attribute_atIndex_effectiveRange_(NSFontAttributeName, index, None) - # we use a couple of magic numbers that seems to work okay. I (BDK) - # got it from some old mozilla code. - height = font.ascender() - font.descender() - height = max(1.0, round(0.05 * height)) - offset = max(1.0, round(0.1 * height)) - return height, offset - -class TextBoxPool(object): - """Handles a pool of TextBox objects. We monitor the TextBox objects and - when those objects die, we reclaim them for the pool. - - Creating TextBoxes is fairly expensive and NSLayoutManager do a lot of - caching, so it's useful to keep them around rather than destroying them. - """ - - def __init__(self): - self.used_text_boxes = [] - self.available_text_boxes = [] - - def get(self): - """Get a NSLayoutManager, either from the pool or by creating a new - one. - """ - try: - rv = self.available_text_boxes.pop() - except IndexError: - rv = TextBox() - self.used_text_boxes.append(rv) - return rv - - def reclaim_textboxes(self): - """Move used TextBoxes back to the available pool. This should be - called after the code using text boxes is done using all of them. - """ - self.available_text_boxes.extend(self.used_text_boxes) - self.used_text_boxes[:] = [] - -text_box_pool = TextBoxPool() - -class Font(object): - line_height_sizer = NSLayoutManager.alloc().init() - - def __init__(self, nsfont): - self.nsfont = nsfont - - def ascent(self): - return self.nsfont.ascender() - - def descent(self): - return -self.nsfont.descender() - - def line_height(self): - return Font.line_height_sizer.defaultLineHeightForFont_(self.nsfont) - -class FontPool(object): - def __init__(self): - self._cached_fonts = {} - - def get(self, scale_factor, bold, italic, family): - cache_key = (scale_factor, bold, italic, family) - try: - return self._cached_fonts[cache_key] - except KeyError: - font = self._create(scale_factor, bold, italic, family) - self._cached_fonts[cache_key] = font - return font - - def _create(self, scale_factor, bold, italic, family): - size = round(scale_factor * NSFont.systemFontSize()) - nsfont = None - if family is not None: - if bold: - nsfont = NSFont.fontWithName_size_(family + " Bold", size) - else: - nsfont = NSFont.fontWithName_size_(family, size) - if nsfont is None: - logging.error('FontPool: family %s scale %s bold %s ' - 'italic %s not found', - family, scale_factor, bold, italic) - # Att his point either we have requested a custom font that failed - # to load or the system font was requested. - if nsfont is None: - if bold: - nsfont = NSFont.boldSystemFontOfSize_(size) - else: - nsfont = NSFont.systemFontOfSize_(size) - return Font(nsfont) - -class LayoutManager(object): - font_pool = FontPool() - default_font = font_pool.get(1.0, False, False, None) - - def __init__(self): - self.current_font = self.default_font - self.set_text_color((0, 0, 0)) - self.set_text_shadow(None) - - def font(self, scale_factor, bold=False, italic=False, family=None): - return self.font_pool.get(scale_factor, bold, italic, family) - - def set_font(self, scale_factor, bold=False, italic=False, family=None): - self.current_font = self.font(scale_factor, bold, italic, family) - - def set_text_color(self, color): - self.text_color = color - - def set_text_shadow(self, shadow): - self.shadow = shadow - - def textbox(self, text, underline=False): - text_box = text_box_pool.get() - color = NSColor.colorWithDeviceRed_green_blue_alpha_(self.text_color[0], self.text_color[1], self.text_color[2], 1.0) - text_box.reset(text, self.current_font, color, self.shadow, underline) - return text_box - - def button(self, text, pressed=False, disabled=False, style='normal'): - if style == 'webby': - return StyledButton(text, self.current_font, pressed, disabled) - else: - return NativeButton(text, self.current_font, pressed, disabled) - - def reset(self): - text_box_pool.reclaim_textboxes() - self.current_font = self.default_font - self.text_color = (0, 0, 0) - self.shadow = None - -class TextBox(object): - def __init__(self): - self.layout_manager = MiroLayoutManager.alloc().init() - container = NSTextContainer.alloc().init() - container.setLineFragmentPadding_(0) - self.layout_manager.addTextContainer_(container) - self.layout_manager.setUsesFontLeading_(NO) - self.text_storage = NSTextStorage.alloc().init() - self.text_storage.addLayoutManager_(self.layout_manager) - self.text_container = self.layout_manager.textContainers()[0] - - def reset(self, text, font, color, shadow, underline): - """Reset the text box so it's ready to be used by a new owner.""" - self.text_storage.deleteCharactersInRange_(NSRange(0, - self.text_storage.length())) - self.text_container.setContainerSize_(NSSize(INFINITE, INFINITE)) - self.paragraph_style = NSMutableParagraphStyle.alloc().init() - self.font = font - self.color = color - self.shadow = shadow - self.width = None - self.set_text(text, underline=underline) - - def make_attr_string(self, text, color, font, underline): - attributes = NSMutableDictionary.alloc().init() - if color is not None: - nscolor = NSColor.colorWithDeviceRed_green_blue_alpha_(color[0], color[1], color[2], 1.0) - attributes.setObject_forKey_(nscolor, NSForegroundColorAttributeName) - else: - attributes.setObject_forKey_(self.color, NSForegroundColorAttributeName) - if font is not None: - attributes.setObject_forKey_(font.nsfont, NSFontAttributeName) - else: - attributes.setObject_forKey_(self.font.nsfont, NSFontAttributeName) - if underline: - attributes.setObject_forKey_(NSUnderlineStyleSingle, NSUnderlineStyleAttributeName) - attributes.setObject_forKey_(self.paragraph_style.copy(), NSParagraphStyleAttributeName) - if text is None: - text = "" - return NSAttributedString.alloc().initWithString_attributes_(text, attributes) - - def set_text(self, text, color=None, font=None, underline=False): - string = self.make_attr_string(text, color, font, underline) - self.text_storage.setAttributedString_(string) - - def append_text(self, text, color=None, font=None, underline=False): - string = self.make_attr_string(text, color, font, underline) - self.text_storage.appendAttributedString_(string) - - def set_width(self, width): - if width is not None: - self.text_container.setContainerSize_(NSSize(width, INFINITE)) - else: - self.text_container.setContainerSize_(NSSize(INFINITE, INFINITE)) - self.width = width - - def update_paragraph_style(self): - attr = NSParagraphStyleAttributeName - value = self.paragraph_style.copy() - rnge = NSMakeRange(0, self.text_storage.length()) - self.text_storage.addAttribute_value_range_(attr, value, rnge) - - def set_wrap_style(self, wrap): - if wrap == 'word': - self.paragraph_style.setLineBreakMode_(NSLineBreakByWordWrapping) - elif wrap == 'char': - self.paragraph_style.setLineBreakMode_(NSLineBreakByCharWrapping) - elif wrap == 'truncated-char': - self.paragraph_style.setLineBreakMode_(NSLineBreakByTruncatingTail) - else: - raise ValueError("Unknown wrap value: %s" % wrap) - self.update_paragraph_style() - - def set_alignment(self, align): - if align == 'left': - self.paragraph_style.setAlignment_(NSLeftTextAlignment) - elif align == 'right': - self.paragraph_style.setAlignment_(NSRightTextAlignment) - elif align == 'center': - self.paragraph_style.setAlignment_(NSCenterTextAlignment) - else: - raise ValueError("Unknown align value: %s" % align) - self.update_paragraph_style() - - def get_size(self): - # The next line is there just to force cocoa to layout the text - self.layout_manager.glyphRangeForTextContainer_(self.text_container) - rect = self.layout_manager.usedRectForTextContainer_(self.text_container) - return rect.size.width, rect.size.height - - def char_at(self, x, y): - width, height = self.get_size() - if 0 <= x < width and 0 <= y < height: - index, _ = self.layout_manager.glyphIndexForPoint_inTextContainer_fractionOfDistanceThroughGlyph_(NSPoint(x, y), self.text_container, None) - return index - else: - return None - - def draw(self, context, x, y, width, height): - if self.shadow is not None: - context.save() - context.set_shadow(self.shadow.color, self.shadow.opacity, self.shadow.offset, self.shadow.blur_radius) - self.width = width - self.text_container.setContainerSize_(NSSize(width, height)) - glyph_range = self.layout_manager.glyphRangeForTextContainer_(self.text_container) - self.layout_manager.drawGlyphsForGlyphRange_atPoint_(glyph_range, NSPoint(x, y)) - if self.shadow is not None: - context.restore() - context.path.removeAllPoints() - -class NativeButton(object): - - def __init__(self, text, font, pressed, disabled=False): - self.min_width = 0 - self.cell = NSButtonCell.alloc().init() - self.cell.setBezelStyle_(NSRoundRectBezelStyle) - self.cell.setButtonType_(NSMomentaryPushInButton) - self.cell.setFont_(font.nsfont) - self.cell.setEnabled_(not disabled) - self.cell.setTitle_(text) - if pressed: - self.cell.setState_(NSOnState) - else: - self.cell.setState_(NSOffState) - self.cell.setImagePosition_(NSImageLeft) - - def set_icon(self, icon): - image = icon.image.copy() - image.setFlipped_(NO) - self.cell.setImage_(image) - - def get_size(self): - size = self.cell.cellSize() - return size.width, size.height - - def draw(self, context, x, y, width, height): - rect = NSMakeRect(x, y, width, height) - NSGraphicsContext.currentContext().saveGraphicsState() - self.cell.drawWithFrame_inView_(rect, context.view) - NSGraphicsContext.currentContext().restoreGraphicsState() - context.path.removeAllPoints() - -class StyledButton(object): - PAD_HORIZONTAL = 11 - BIG_PAD_VERTICAL = 4 - SMALL_PAD_VERTICAL = 2 - TOP_COLOR = (1, 1, 1) - BOTTOM_COLOR = (0.86, 0.86, 0.86) - LINE_COLOR_TOP = (0.71, 0.71, 0.71) - LINE_COLOR_BOTTOM = (0.45, 0.45, 0.45) - TEXT_COLOR = (0.19, 0.19, 0.19) - DISABLED_COLOR = (0.86, 0.86, 0.86) - DISABLED_TEXT_COLOR = (0.43, 0.43, 0.43) - ICON_PAD = 8 - - def __init__(self, text, font, pressed, disabled=False): - self.pressed = pressed - self.disabled = disabled - attributes = NSMutableDictionary.alloc().init() - attributes.setObject_forKey_(font.nsfont, NSFontAttributeName) - if self.disabled: - color = self.DISABLED_TEXT_COLOR - else: - color = self.TEXT_COLOR - nscolor = NSColor.colorWithDeviceRed_green_blue_alpha_(color[0], color[1], color[2], 1.0) - attributes.setObject_forKey_(nscolor, NSForegroundColorAttributeName) - self.title = NSAttributedString.alloc().initWithString_attributes_(text, attributes) - self.image = None - - def set_icon(self, icon): - self.image = icon.image.copy() - self.image.setFlipped_(YES) - - def get_size(self): - width, height = self.get_text_size() - if self.image is not None: - width += self.image.size().width + self.ICON_PAD - height = max(height, self.image.size().height) - height += self.BIG_PAD_VERTICAL * 2 - else: - height += self.SMALL_PAD_VERTICAL * 2 - if height % 2 == 1: - # make height even so that the radius of our circle is whole - height += 1 - width += self.PAD_HORIZONTAL * 2 - return width, height - - def get_text_size(self): - size = self.title.size() - return size.width, size.height - - def draw(self, context, x, y, width, height): - self._draw_button(context, x, y, width, height) - self._draw_title(context, x, y) - context.path.removeAllPoints() - - def _draw_button(self, context, x, y, width, height): - radius = height / 2 - self._draw_path(context, x, y, width, height, radius) - if self.disabled: - end_color = self.DISABLED_COLOR - start_color = self.DISABLED_COLOR - elif self.pressed: - end_color = self.TOP_COLOR - start_color = self.BOTTOM_COLOR - else: - context.set_line_width(1) - start_color = self.TOP_COLOR - end_color = self.BOTTOM_COLOR - gradient = drawing.Gradient(x, y, x, y+height) - gradient.set_start_color(start_color) - gradient.set_end_color(end_color) - context.gradient_fill(gradient) - self._draw_border(context, x, y, width, height, radius) - - def _draw_path(self, context, x, y, width, height, radius): - inner_width = width - radius * 2 - context.move_to(x + radius, y) - context.rel_line_to(inner_width, 0) - context.arc(x + width - radius, y+radius, radius, -math.pi/2, math.pi/2) - context.rel_line_to(-inner_width, 0) - context.arc(x + radius, y+radius, radius, math.pi/2, -math.pi/2) - - def _draw_path_reverse(self, context, x, y, width, height, radius): - inner_width = width - radius * 2 - context.move_to(x + radius, y) - context.arc_negative(x + radius, y+radius, radius, -math.pi/2, math.pi/2) - context.rel_line_to(inner_width, 0) - context.arc_negative(x + width - radius, y+radius, radius, math.pi/2, -math.pi/2) - context.rel_line_to(-inner_width, 0) - - def _draw_border(self, context, x, y, width, height, radius): - self._draw_path(context, x, y, width, height, radius) - self._draw_path_reverse(context, x+1, y+1, width-2, height-2, radius-1) - gradient = drawing.Gradient(x, y, x, y+height) - gradient.set_start_color(self.LINE_COLOR_TOP) - gradient.set_end_color(self.LINE_COLOR_BOTTOM) - context.save() - context.clip() - context.rectangle(x, y, width, height) - context.gradient_fill(gradient) - context.restore() - - def _draw_title(self, context, x, y): - c_width, c_height = self.get_size() - t_width, t_height = self.get_text_size() - x = x + self.PAD_HORIZONTAL - y = y + (c_height - t_height) / 2 - if self.image is not None: - self.image.drawAtPoint_fromRect_operation_fraction_( - NSPoint(x, y+3), NSZeroRect, NSCompositeSourceOver, 1.0) - x += self.image.size().width + self.ICON_PAD - else: - y += 0.5 - self.title.drawAtPoint_(NSPoint(x, y)) diff --git a/mvc/widgets/osx/osxmenus.py b/mvc/widgets/osx/osxmenus.py deleted file mode 100644 index 32ca469..0000000 --- a/mvc/widgets/osx/osxmenus.py +++ /dev/null @@ -1,571 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -"""menus.py -- Menu handling code.""" - -import logging -import struct - -from objc import nil, NO, YES -import AppKit -from AppKit import * -from Foundation import * - -from mvc import signals -from mvc.widgets import keyboard -# import these names directly into our namespace for easy access -from mvc.widgets.keyboard import Shortcut, MOD - -# XXX hacks -def _(text, *params): - if params: - return text % params[0] - return text - -MODIFIERS_MAP = { - keyboard.MOD: NSCommandKeyMask, - keyboard.CMD: NSCommandKeyMask, - keyboard.SHIFT: NSShiftKeyMask, - keyboard.CTRL: NSControlKeyMask, - keyboard.ALT: NSAlternateKeyMask -} - -if isinstance(NSBackspaceCharacter, int): - backspace = NSBackspaceCharacter -else: - backspace = ord(NSBackspaceCharacter) - -KEYS_MAP = { - keyboard.SPACE: " ", - keyboard.ENTER: "\r", - keyboard.BKSPACE: struct.pack("H", backspace), - keyboard.DELETE: NSDeleteFunctionKey, - keyboard.RIGHT_ARROW: NSRightArrowFunctionKey, - keyboard.LEFT_ARROW: NSLeftArrowFunctionKey, - keyboard.UP_ARROW: NSUpArrowFunctionKey, - keyboard.DOWN_ARROW: NSDownArrowFunctionKey, - '.': '.', - ',': ',' -} -# add function keys -for i in range(1, 13): - portable_key = getattr(keyboard, "F%s" % i) - osx_key = getattr(AppKit, "NSF%sFunctionKey" % i) - KEYS_MAP[portable_key] = osx_key - -REVERSE_MODIFIERS_MAP = dict((i[1], i[0]) for i in MODIFIERS_MAP.items()) -REVERSE_KEYS_MAP = dict((i[1], i[0]) for i in KEYS_MAP.items() - if i[0] != keyboard.BKSPACE) -REVERSE_KEYS_MAP[u'\x7f'] = keyboard.BKSPACE -REVERSE_KEYS_MAP[u'\x1b'] = keyboard.ESCAPE - -def make_modifier_mask(shortcut): - mask = 0 - for modifier in shortcut.modifiers: - mask |= MODIFIERS_MAP[modifier] - return mask - -VIEW_ITEM_MAP = {} - -def _remove_mnemonic(label): - """Remove the underscore used by GTK for mnemonics. - - We totally ignore them on OSX, since they are now deprecated. - """ - return label.replace("_", "") - -def handle_menu_activate(ns_menu_item): - """Handle a menu item being activated. - - This gets called by our application delegate. - """ - - menu_item = ns_menu_item.representedObject() - menu_item.emit("activate") - menubar = menu_item._find_menubar() - if menubar is not None: - menubar.emit("activate", menu_item.name) - -class MenuItemBase(signals.SignalEmitter): - """Base class for MenuItem and Separator""" - def __init__(self): - signals.SignalEmitter.__init__(self) - self.name = None - self.parent = None - - def show(self): - self._menu_item.setHidden_(False) - - def hide(self): - self._menu_item.setHidden_(True) - - def enable(self): - self._menu_item.setEnabled_(True) - - def disable(self): - self._menu_item.setEnabled_(False) - - def remove_from_parent(self): - """Remove this menu item from it's parent Menu.""" - if self.parent is not None: - self.parent.remove(self) - -class MenuItem(MenuItemBase): - """See the GTK version of this method for the current docstring.""" - - # map Miro action names to standard OSX actions. - _STD_ACTION_MAP = { - "HideMiro": (NSApp(), 'hide:'), - "HideOthers": (NSApp(), 'hideOtherApplications:'), - "ShowAll": (NSApp(), 'unhideAllApplications:'), - "Cut": (nil, 'cut:'), - "Copy": (nil, 'copy:'), - "Paste": (nil, 'paste:'), - "Delete": (nil, 'delete:'), - "SelectAll": (nil, 'selectAll:'), - "Zoom": (nil, 'performZoom:'), - "Minimize": (nil, 'performMiniaturize:'), - "BringAllToFront": (nil, 'arrangeInFront:'), - "CloseWindow": (nil, 'performClose:'), - } - - def __init__(self, label, name, shortcut=None): - MenuItemBase.__init__(self) - self.name = name - self._menu_item = self._make_menu_item(label) - self.create_signal('activate') - self._setup_shortcut(shortcut) - - def _make_menu_item(self, label): - menu_item = NSMenuItem.alloc().init() - menu_item.setTitle_(_remove_mnemonic(label)) - # we set ourselves as the represented object for the menu item so we - # can easily translate one to the other - menu_item.setRepresentedObject_(self) - if self.name in self._STD_ACTION_MAP: - menu_item.setTarget_(self._STD_ACTION_MAP[self.name][0]) - menu_item.setAction_(self._STD_ACTION_MAP[self.name][1]) - else: - menu_item.setTarget_(NSApp().delegate()) - menu_item.setAction_('handleMenuActivate:') - return menu_item - - def _setup_shortcut(self, shortcut): - if shortcut is None: - key = '' - modifier_mask = 0 - elif isinstance(shortcut.shortcut, str): - key = shortcut.shortcut - modifier_mask = make_modifier_mask(shortcut) - elif shortcut.shortcut in KEYS_MAP: - key = KEYS_MAP[shortcut.shortcut] - modifier_mask = make_modifier_mask(shortcut) - else: - logging.warn("Don't know how to handle shortcut: %s", shortcut) - return - self._menu_item.setKeyEquivalent_(key) - self._menu_item.setKeyEquivalentModifierMask_(modifier_mask) - - def _change_shortcut(self, shortcut): - self._setup_shortcut(shortcut) - - def set_label(self, new_label): - self._menu_item.setTitle_(new_label) - - def get_label(self): - self._menu_item.title() - - def _find_menubar(self): - """Remove this menu item from it's parent Menu.""" - menu_item = self - while menu_item.parent is not None: - menu_item = menu_item.parent - if isinstance(menu_item, MenuBar): - return menu_item - else: - return None - -class CheckMenuItem(MenuItem): - """See the GTK version of this method for the current docstring.""" - def set_state(self, active): - if active is None: - state = NSMixedState - elif active: - state = NSOnState - else: - state = NSOffState - self._menu_item.setState_(state) - - def get_state(self): - return self._menu_item.state() == NSOnState - - def do_activate(self): - if self._menu_item.state() == NSOffState: - self._menu_item.setState_(NSOnState) - else: - self._menu_item.setState_(NSOffState) - -class RadioMenuItem(CheckMenuItem): - """See the GTK version of this method for the current docstring.""" - def __init__(self, label, name, shortcut=None): - CheckMenuItem.__init__(self, label, name, shortcut) - # The leader of a radio group stores the list of all items in the - # group - self.group_leader = None - self.others_in_group = set() - - def set_group(self, group_item): - if self.group_leader is not None: - raise ValueError("%s is already in a group" % self) - if group_item.group_leader is None: - group_leader = group_item - else: - group_leader = group_item.group_leader - if group_leader.group_leader is not None: - raise AssertionError("group_leader structure is wrong") - self.group_leader = group_leader - group_leader.others_in_group.add(self) - - def remove_from_group(self): - """Remove this RadioMenuItem from its current group.""" - if self.group_leader is not None: - # we have a group leader, remove ourself from their list. - # Note that this code will work even if we're the last item in - # others_in_group. - self.group_leader.others_in_group.remove(self) - self.group_leader = None - elif len(self.others_in_group) > 1: - # we're the group leader, hand off the leader to a different item - first_item = iter(self.others_in_group).next() - for other in self.others_in_group: - if other is first_item: - other.others_in_group = self.others_in_group - other.others_in_group.remove(first_item) - other.group_leader = None - else: - other.group_leader = first_item - self.others_in_group = set() - elif len(self.others_in_group) == 1: - # we're the group leader, but there's only 1 other item. unset - # everything. - for other in self.others_in_group: - other.group_leader = None - self.others_in_group = set() - - def _items_in_group(self): - if self.group_leader is not None: # we have a group leader - yield self.group_leader - for other in self.group_leader.others_in_group: - yield other - elif self.others_in_group: # we're the group leader - yield self - for other in self.others_in_group: - yield other - else: # we don't have a group set - yield self - - def do_activate(self): - for item in self._items_in_group(): - if item is not self: - item.set_state(False) - CheckMenuItem.do_activate(self) - -class Separator(MenuItemBase): - """See the GTK version of this method for the current docstring.""" - def __init__(self): - MenuItemBase.__init__(self) - self._menu_item = NSMenuItem.separatorItem() - -class MenuShell(signals.SignalEmitter): - def __init__(self, nsmenu): - signals.SignalEmitter.__init__(self) - self._menu = nsmenu - self.children = [] - self.parent = None - - def append(self, menu_item): - """Add a menu item to the end of this menu.""" - self.children.append(menu_item) - self._menu.addItem_(menu_item._menu_item) - menu_item.parent = self - - def insert(self, index, menu_item): - """Insert a menu item in the middle of this menu.""" - self.children.insert(index, menu_item) - self._menu.insertItem_atIndex_(menu_item._menu_item, index) - menu_item.parent = self - - def index(self, name): - """Find the position of a child menu item.""" - for i, menu_item in enumerate(self.children): - if menu_item.name == name: - return i - return -1 - - def remove(self, menu_item): - """Remove a child menu item. - - :raises ValueError: menu_item is not a child of this menu - """ - self.children.remove(menu_item) - self._menu.removeItem_(menu_item._menu_item) - menu_item.parent = None - - def get_children(self): - """Get the child menu items in order.""" - return list(self.children) - - def find(self, name): - """Search for a menu or menu item - - This method recursively searches the entire menu structure for a Menu - or MenuItem object with a given name. - - :raises KeyError: name not found - """ - found = self._find(name) - if found is None: - raise KeyError(name) - else: - return found - - def _find(self, name): - """Low-level helper-method for find(). - - :returns: found menu item or None. - """ - for menu_item in self.get_children(): - if menu_item.name == name: - return menu_item - if isinstance(menu_item, Menu): - submenu_find = menu_item._find(name) - if submenu_find is not None: - return submenu_find - return None - -class Menu(MenuShell): - """See the GTK version of this method for the current docstring.""" - def __init__(self, label, name, child_items=None): - MenuShell.__init__(self, NSMenu.alloc().init()) - self._menu.setTitle_(_remove_mnemonic(label)) - # we will enable/disable menu items manually - self._menu.setAutoenablesItems_(False) - self.name = name - if child_items is not None: - for item in child_items: - self.append(item) - self._menu_item = NSMenuItem.alloc().init() - self._menu_item.setTitle_(_remove_mnemonic(label)) - self._menu_item.setSubmenu_(self._menu) - # Hack to set the services menu - if name == "ServicesMenu": - NSApp().setServicesMenu_(self._menu_item) - - def show(self): - self._menu_item.setHidden_(False) - - def hide(self): - self._menu_item.setHidden_(True) - - def enable(self): - self._menu_item.setEnabled_(True) - - def disable(self): - self._menu_item.setEnabled_(False) - -class AppMenu(MenuShell): - """Wrapper for the application menu (AKA the Miro menu) - - We need to special case this because OSX automatically creates the menu - item. - """ - def __init__(self): - MenuShell.__init__(self, NSApp().mainMenu().itemAtIndex_(0).submenu()) - self.name = "Libre Video Converter" - -class MenuBar(MenuShell): - """See the GTK version of this method for the current docstring.""" - def __init__(self): - MenuShell.__init__(self, NSApp().mainMenu()) - self.create_signal('activate') - self._add_app_menu() - - def _add_app_menu(self): - """Add the app menu to this menu bar. - - We need to special case this because OSX automatically adds the - NSMenuItem for the app menu, we just need to set up our wrappers. - """ - self._app_menu = AppMenu() - self.children.append(self._app_menu) - self._app_menu.parent = self - - def add_initial_menus(self, menus): - for menu in menus: - self.append(menu) - self._modify_initial_menus() - - def _extract_menu_item(self, name): - """Helper method for changing the portable menu structure.""" - menu_item = self.find(name) - menu_item.remove_from_parent() - return menu_item - - def _modify_initial_menus(self): - short_appname = "Libre Video Converter" # XXX - - # Application menu - miroMenuItems = [ - self._extract_menu_item("About"), - Separator(), - self._extract_menu_item("Quit") - ] - - for item in miroMenuItems: - self._app_menu.append(item) - - self._app_menu.find("Quit").set_label(_("Quit %(appname)s", - {"appname": short_appname})) - - # Help Menu - #helpItem = self.find("Help") - #helpItem.set_label(_("%(appname)s Help", {"appname": short_appname})) - #helpItem._change_shortcut(Shortcut("?", MOD)) - - self._update_present_menu() - self._connect_to_signals() - - def do_activate(self, name): - # We handle a couple OSX-specific actions here - if name == "PresentActualSize": - NSApp().delegate().present_movie('natural-size') - elif name == "PresentDoubleSize": - NSApp().delegate().present_movie('double-size') - elif name == "PresentHalfSize": - NSApp().delegate().present_movie('half-size') - elif name == "ShowMain": - app.widgetapp.window.nswindow.makeKeyAndOrderFront_(self) - - def _connect_to_signals(self): - return - app.playback_manager.connect("will-play", self._on_playback_change) - app.playback_manager.connect("will-stop", self._on_playback_change) - - def _on_playback_change(self, playback_manager, *args): - self._update_present_menu() - - def _update_present_menu(self): - return - if self._should_enable_present_menu(): - for menu_item in self.present_menu.get_children(): - menu_item.enable() - else: - for menu_item in self.present_menu.get_children(): - menu_item.disable() - - def _should_enable_present_menu(self): - return False - if (app.playback_manager.is_playing and - not app.playback_manager.is_playing_audio): - # we're currently playing video, allow the user to fullscreen - return True - selection_info = app.item_list_controller_manager.get_selection_info() - if (selection_info.has_download and - selection_info.has_file_type('video')): - # A downloaded video is selected, allow the user to start playback - # in fullscreen - return True - return False - -#class ContextMenuHandler(NSObject): -# def initWithCallback_(self, callback): -# self = super(ContextMenuHandler, self).init() -# self.callback = callback -# return self -# -# def handleMenuItem_(self, sender): -# self.callback() -# -#class MiroContextMenu(NSMenu): -# # Works exactly like NSMenu, except it keeps a reference to the menu -# # handler objects. -# def init(self): -# self = super(MiroContextMenu, self).init() -# self.handlers = set() -# return self -# -# def addItem_(self, item): -# if isinstance(item.target(), ContextMenuHandler): -# self.handlers.add(item.target()) -# return NSMenu.addItem_(self, item) -# -def make_context_menu(menu_items): - nsmenu = MiroContextMenu.alloc().init() - for item in menu_items: - if item is None: - nsitem = NSMenuItem.separatorItem() - else: - label, callback = item - nsitem = NSMenuItem.alloc().init() - if isinstance(label, tuple) and len(label) == 2: - label, icon_path = label - image = NSImage.alloc().initWithContentsOfFile_(icon_path) - nsitem.setImage_(image) - if callback is None: - font_size = NSFont.systemFontSize() - font = NSFont.fontWithName_size_("Lucida Sans Italic", font_size) - if font is None: - font = NSFont.systemFontOfSize_(font_size) - attributes = {NSFontAttributeName: font} - attributed_label = NSAttributedString.alloc().initWithString_attributes_(label, attributes) - nsitem.setAttributedTitle_(attributed_label) - else: - nsitem.setTitle_(label) - if isinstance(callback, list): - submenu = make_context_menu(callback) - nsmenu.setSubmenu_forItem_(submenu, nsitem) - else: - handler = ContextMenuHandler.alloc().initWithCallback_(callback) - nsitem.setTarget_(handler) - nsitem.setAction_('handleMenuItem:') - nsmenu.addItem_(nsitem) - return nsmenu - -def translate_event_modifiers(event): - mods = set() - flags = event.modifierFlags() - if flags & NSCommandKeyMask: - mods.add(keyboard.CMD) - if flags & NSControlKeyMask: - mods.add(keyboard.CTRL) - if flags & NSAlternateKeyMask: - mods.add(keyboard.ALT) - if flags & NSShiftKeyMask: - mods.add(keyboard.SHIFT) - return mods diff --git a/mvc/widgets/osx/rect.py b/mvc/widgets/osx/rect.py deleted file mode 100644 index 3c8d448..0000000 --- a/mvc/widgets/osx/rect.py +++ /dev/null @@ -1,78 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -""".rect -- Simple Rectangle class.""" - -from Foundation import NSMakeRect, NSRectFromString - -class Rect(object): - @classmethod - def from_string(cls, rect_string): - if rect_string.startswith('{{'): - return NSRectWrapper(NSRectFromString(rect_string)) - else: - try: - items = [int(i) for i in rect_string.split(',')] - return Rect(*items) - except: - return None - - def __init__(self, x, y, width, height): - self.nsrect = NSMakeRect(x, y, width, height) - - def get_x(self): - return self.nsrect.origin.x - def set_x(self, x): - self.nsrect.origin.x = x - x = property(get_x, set_x) - - def get_y(self): - return self.nsrect.origin.y - def set_y(self, y): - self.nsrect.origin.x = y - y = property(get_y, set_y) - - def get_width(self): - return self.nsrect.size.width - def set_width(self, width): - self.nsrect.size.width = width - width = property(get_width, set_width) - - def get_height(self): - return self.nsrect.size.height - def set_height(self, height): - self.nsrect.size.height = height - height = property(get_height, set_height) - - def __str__(self): - return "%d,%d,%d,%d" % (self.nsrect.origin.x, self.nsrect.origin.y, self.nsrect.size.width, self.nsrect.size.height) - -class NSRectWrapper(Rect): - def __init__(self, nsrect): - self.nsrect = nsrect diff --git a/mvc/widgets/osx/simple.py b/mvc/widgets/osx/simple.py deleted file mode 100644 index 1c12b06..0000000 --- a/mvc/widgets/osx/simple.py +++ /dev/null @@ -1,376 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -from __future__ import division -import logging -import math - -from AppKit import * -from Foundation import * -from objc import YES, NO, nil - -from mvc.widgets import widgetconst -from .base import Widget, SimpleBin, FlippedView -from .utils import filename_to_unicode -import drawing -import wrappermap - -"""A collection of various simple widgets.""" - -class Image(object): - """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class.""" - def __init__(self, path): - self._set_image(NSImage.alloc().initByReferencingFile_( - filename_to_unicode(path))) - - def _set_image(self, nsimage): - self.nsimage = nsimage - self.width = self.nsimage.size().width - self.height = self.nsimage.size().height - if self.width * self.height == 0: - raise ValueError('Image has invalid size: (%d, %d)' % ( - self.width, self.height)) - - def resize(self, width, height): - return ResizedImage(self, width, height) - - def crop_and_scale(self, src_x, src_y, src_width, src_height, dest_width, - dest_height): - if dest_width <= 0 or dest_height <= 0: - logging.stacktrace("invalid dest sizes: %s %s" % (dest_width, - dest_height)) - return TransformedImage(self.nsimage) - - source_rect = NSMakeRect(src_x, src_y, src_width, src_height) - dest_rect = NSMakeRect(0, 0, dest_width, dest_height) - - dest = NSImage.alloc().initWithSize_(NSSize(dest_width, dest_height)) - dest.lockFocus() - try: - NSGraphicsContext.currentContext().setImageInterpolation_( - NSImageInterpolationHigh) - self.nsimage.drawInRect_fromRect_operation_fraction_(dest_rect, - source_rect, NSCompositeCopy, 1.0) - finally: - dest.unlockFocus() - return TransformedImage(dest) - - def resize_for_space(self, width, height): - """Returns an image scaled to fit into the specified space at the - correct height/width ratio. - """ - # this prevents division by 0. - if self.width == 0 and self.height == 0: - return self - elif self.width == 0: - ratio = height / self.height - return self.resize(self.width, ratio * self.height) - elif self.height == 0: - ratio = width / self.width - return self.resize(ratio * self.width, self.height) - - ratio = min(width / self.width, height / self.height) - return self.resize(ratio * self.width, ratio * self.height) - -class ResizedImage(Image): - def __init__(self, image, width, height): - nsimage = image.nsimage.copy() - nsimage.setCacheMode_(NSImageCacheNever) - nsimage.setScalesWhenResized_(YES) - nsimage.setSize_(NSSize(width, height)) - self._set_image(nsimage) - -class TransformedImage(Image): - def __init__(self, nsimage): - self._set_image(nsimage) - -class NSImageDisplay(NSView): - def init(self): - self = super(NSImageDisplay, self).init() - self.border = False - self.image = None - return self - - def isFlipped(self): - return YES - - def set_border(self, border): - self.border = border - - def set_image(self, image): - self.image = image - - def drawRect_(self, dest_rect): - if self.image is not None: - source_rect = self.calculateSourceRectFromDestRect_(dest_rect) - context = NSGraphicsContext.currentContext() - context.setShouldAntialias_(YES) - context.setImageInterpolation_(NSImageInterpolationHigh) - context.saveGraphicsState() - drawing.flip_context(self.bounds().size.height) - self.image.nsimage.drawInRect_fromRect_operation_fraction_( - dest_rect, source_rect, NSCompositeSourceOver, 1.0) - context.restoreGraphicsState() - if self.border: - context = drawing.DrawingContext(self, self.bounds(), dest_rect) - context.style = drawing.DrawingStyle() - context.set_line_width(1) - context.set_color((0, 0, 0)) # black - context.rectangle(0, 0, context.width, context.height) - context.stroke() - - def calculateSourceRectFromDestRect_(self, dest_rect): - """Calulate where dest_rect maps to on our image. - - This is tricky because our image might be scaled up, in which case - the rect from our image will be smaller than dest_rect. - """ - view_size = self.frame().size - x_scale = float(self.image.width) / view_size.width - y_scale = float(self.image.height) / view_size.height - - return NSMakeRect(dest_rect.origin.x * x_scale, - dest_rect.origin.y * y_scale, - dest_rect.size.width * x_scale, - dest_rect.size.height * y_scale) - - # XXX FIXME: should track mouse movement - mouseDown is not the correct - # event. - def mouseDown_(self, event): - wrappermap.wrapper(self).emit('clicked') - -class ImageDisplay(Widget): - """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class.""" - def __init__(self, image=None): - Widget.__init__(self) - self.create_signal('clicked') - self.view = NSImageDisplay.alloc().init() - self.set_image(image) - - def set_image(self, image): - self.image = image - if image: - image.nsimage.setCacheMode_(NSImageCacheNever) - self.view.set_image(image) - self.invalidate_size_request() - - def set_border(self, border): - self.view.set_border(border) - - def calc_size_request(self): - if self.image is not None: - return self.image.width, self.image.height - else: - return 0, 0 - -class ClickableImageButton(ImageDisplay): - def __init__(self, image_path, max_width=None, max_height=None): - ImageDisplay.__init__(self) - self.set_border(True) - self.max_width = max_width - self.max_height = max_height - self.image = None - self._width, self._height = None, None - if image_path: - self.set_path(image_path) - - def set_path(self, path): - image = Image(path) - if self.max_width: - image = image.resize_for_space(self.max_width, self.max_height) - super(ClickableImageButton, self).set_image(image) - - def calc_size_request(self): - if self.max_width: - return self.max_width, self.max_height - else: - return ImageDisplay.calc_size_request(self) - -class MiroImageView(NSImageView): - def viewWillMoveToWindow_(self, aWindow): - self.setAnimates_(not aWindow == nil) - -class AnimatedImageDisplay(Widget): - def __init__(self, path): - Widget.__init__(self) - self.nsimage = NSImage.alloc().initByReferencingFile_( - filename_to_unicode(path)) - self.view = MiroImageView.alloc().init() - self.view.setImage_(self.nsimage) - # enabled when viewWillMoveToWindow:aWindow invoked - self.view.setAnimates_(NO) - - def calc_size_request(self): - return self.nsimage.size().width, self.nsimage.size().height - -class Label(Widget): - """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class.""" - def __init__(self, text="", wrap=False, color=None): - Widget.__init__(self) - self.view = NSTextField.alloc().init() - self.view.setEditable_(NO) - self.view.setBezeled_(NO) - self.view.setBordered_(NO) - self.view.setDrawsBackground_(NO) - self.wrap = wrap - self.bold = False - self.size = NSFont.systemFontSize() - self.sizer_cell = self.view.cell().copy() - self.set_font() - self.set_text(text) - self.__color = self.view.textColor() - if color is not None: - self.set_color(color) - - def get_width(self): - return self.calc_size_request()[0] - - def set_bold(self, bold): - self.bold = bold - self.set_font() - - def set_size(self, size): - if size > 0: - self.size = NSFont.systemFontSize() * size - elif size == widgetconst.SIZE_SMALL: - self.size = NSFont.smallSystemFontSize() - elif size == widgetconst.SIZE_NORMAL: - self.size = NSFont.systemFontSize() - else: - raise ValueError("Unknown size constant: %s" % size) - - self.set_font() - - def set_color(self, color): - self.__color = self.make_color(color) - - if self.view.isEnabled(): - self.view.setTextColor_(self.__color) - else: - self.view.setTextColor_(self.__color.colorWithAlphaComponent_(0.5)) - - def set_background_color(self, color): - self.view.setBackgroundColor_(self.make_color(color)) - self.view.setDrawsBackground_(YES) - - def set_font(self): - if self.bold: - font = NSFont.boldSystemFontOfSize_(self.size) - else: - font= NSFont.systemFontOfSize_(self.size) - self.view.setFont_(font) - self.sizer_cell.setFont_(font) - self.invalidate_size_request() - - def calc_size_request(self): - if (self.wrap and self.manual_size_request is not None and - self.manual_size_request[0] > 0): - wrap_width = self.manual_size_request[0] - size = self.sizer_cell.cellSizeForBounds_(NSMakeRect(0, 0, - wrap_width, 10000)) - else: - size = self.sizer_cell.cellSize() - return math.ceil(size.width), math.ceil(size.height) - - def baseline(self): - return -self.view.font().descender() - - def set_text(self, text): - self.view.setStringValue_(text) - self.sizer_cell.setStringValue_(text) - self.invalidate_size_request() - - def get_text(self): - val = self.view.stringValue() - if not val: - val = u'' - return val - - def set_selectable(self, val): - self.view.setSelectable_(val) - - def set_alignment(self, alignment): - self.view.setAlignment_(alignment) - - def get_alignment(self, alignment): - return self.view.alignment() - - def set_wrap(self, wrap): - self.wrap = True - self.invalidate_size_request() - - def enable(self): - Widget.enable(self) - self.view.setTextColor_(self.__color) - self.view.setEnabled_(True) - - def disable(self): - Widget.disable(self) - self.view.setTextColor_(self.__color.colorWithAlphaComponent_(0.5)) - self.view.setEnabled_(False) - -class SolidBackground(SimpleBin): - def __init__(self, color=None): - SimpleBin.__init__(self) - self.view = FlippedView.alloc().init() - if color is not None: - self.set_background_color(color) - - def set_background_color(self, color): - self.view.setBackgroundColor_(self.make_color(color)) - -class ProgressBar(Widget): - def __init__(self): - Widget.__init__(self) - self.view = NSProgressIndicator.alloc().init() - self.view.setMaxValue_(1.0) - self.view.setIndeterminate_(False) - - def calc_size_request(self): - return 20, 20 - - def set_progress(self, fraction): - self.view.setIndeterminate_(False) - self.view.setDoubleValue_(fraction) - - def start_pulsing(self): - self.view.setIndeterminate_(True) - self.view.startAnimation_(nil) - - def stop_pulsing(self): - self.view.stopAnimation_(nil) - -class HLine(Widget): - def __init__(self): - Widget.__init__(self) - self.view = NSBox.alloc().init() - self.view.setBoxType_(NSBoxSeparator) - - def calc_size_request(self): - return self.view.frame().size.width, self.view.frame().size.height diff --git a/mvc/widgets/osx/tablemodel.py b/mvc/widgets/osx/tablemodel.py deleted file mode 100644 index 980b60b..0000000 --- a/mvc/widgets/osx/tablemodel.py +++ /dev/null @@ -1,532 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -"""tablemodel.py -- Model classes for TableView. """ - -import logging - -from AppKit import (NSDragOperationNone, NSDragOperationAll, NSTableViewDropOn, - NSOutlineViewDropOnItemIndex, protocols) -from Foundation import NSObject, NSNotFound, NSMutableIndexSet -from objc import YES, NO, nil - -from mvc import signals -from mvc.errors import WidgetActionError -import fasttypes -import wrappermap - -MIRO_DND_ITEM_LOCAL = 'miro-local-item' - -# XXX need unsigned but value comes out as signed. -NSDragOperationEvery = NSDragOperationAll - -def list_from_nsindexset(index_set): - rows = list() - index = index_set.firstIndex() - while (index != NSNotFound): - rows.append(index) - index = index_set.indexGreaterThanIndex_(index) - return rows - -class RowList(object): - """RowList is a Linked list that has some optimizations for looking up - rows by index number. - """ - def __init__(self): - self.list = fasttypes.LinkedList() - self.iter_cache = [] - - def firstIter(self): - return self.list.firstIter() - - def lastIter(self): - return self.list.lastIter() - - def insertBefore(self, iter, value): - self.iter_cache = [] - if iter is None: - return self.list.append(value) - else: - return self.list.insertBefore(iter, value) - - def append(self, value): - return self.list.append(value) - - def __len__(self): - return len(self.list) - - def __getitem__(self, iter): - return self.list[iter] - - def __iter__(self): - iter = self.firstIter() - while iter != self.lastIter(): - yield iter.value() - iter.forward() - - def remove(self, iter): - self.iter_cache = [] - return self.list.remove(iter) - - def nth_iter(self, index): - if index < 0: - raise IndexError(index) - elif index >= len(self): - raise LookupError() - if len(self.iter_cache) == 0: - self.iter_cache.append(self.firstIter()) - try: - return self.iter_cache[index].copy() - except IndexError: - pass - iter = self.iter_cache[-1].copy() - index -= len(self.iter_cache) - 1 - for x in xrange(index): - iter.forward() - self.iter_cache.append(iter.copy()) - return iter - -class TableModelBase(signals.SignalEmitter): - """Base class for TableModel and TreeTableModel.""" - def __init__(self, *column_types): - signals.SignalEmitter.__init__(self) - self.row_list = RowList() - self.column_types = column_types - self.create_signal('row-changed') - self.create_signal('structure-will-change') - - def check_column_values(self, column_values): - if len(self.column_types) != len(column_values): - raise ValueError("Wrong number of columns") - # We might want to do more typechecking here - - def get_column_data(self, row, column): - attr_map = column.attrs() - return dict((name, row[index]) for name, index in attr_map.items()) - - def update_value(self, iter, index, value): - iter.value().values[index] = value - self.emit('row-changed', iter) - - def update(self, iter, *column_values): - iter.value().update_values(column_values) - self.emit('row-changed', iter) - - def remove(self, iter): - self.emit('structure-will-change') - row_list = self.containing_list(iter) - rv = row_list.remove(iter) - if rv == row_list.lastIter(): - rv = None - return rv - - def nth_iter(self, index): - return self.row_list.nth_iter(index) - - def next_iter(self, iter): - row_list = self.containing_list(iter) - retval = iter.copy() - retval.forward() - if retval == row_list.lastIter(): - return None - else: - return retval - - def first_iter(self): - if len(self.row_list) > 0: - return self.row_list.firstIter() - else: - return None - - def __len__(self): - return len(self.row_list) - - def __getitem__(self, iter): - return iter.value() - - def __iter__(self): - return iter(self.row_list) - -class TableRow(object): - """See https://develop.participatoryculture.org/index.php/WidgetAPITableView for a description of the API for this class.""" - def __init__(self, column_values): - self.update_values(column_values) - - def update_values(self, column_values): - self.values = list(column_values) - - def __getitem__(self, index): - return self.values[index] - - def __len__(self): - return len(self.values) - - def __iter__(self): - return iter(self.values) - -class TableModel(TableModelBase): - """See https://develop.participatoryculture.org/index.php/WidgetAPITableView for a description of the API for this class.""" - def __init__(self, *column_types): - TableModelBase.__init__(self, column_types) - self.row_indexes = {} - - def remember_row_at_index(self, row, index): - if row not in self.row_indexes: - self.row_indexes[row] = index - - def row_of_iter(self, tableview, iter): - row = iter.value() - try: - return self.row_indexes[row] - except KeyError: - iter = self.row_list.firstIter() - index = 0 - while iter != self.row_list.lastIter(): - current_row = iter.value() - self.row_indexes[current_row] = index - if current_row is row: - return index - index += 1 - iter.forward() - raise LookupError("%s is not in this table" % row) - - def containing_list(self, iter): - return self.row_list - - def append(self, *column_values): - self.emit('structure-will-change') - self.row_indexes = {} - retval = self.row_list.append(TableRow(column_values)) - return retval - - def remove(self, iter): - self.row_indexes = {} - return TableModelBase.remove(self, iter) - - def insert_before(self, iter, *column_values): - self.emit('structure-will-change') - self.row_indexes = {} - row = TableRow(column_values) - retval = self.row_list.insertBefore(iter, row) - return retval - - def iter_for_row(self, tableview, row): - return self.row_list.nth_iter(row) - - -class TreeNode(NSObject, TableRow): - """A row in a TreeTableModel""" - - # Implementation note: these need to be NSObjects because we return them - # to the NSOutlineView. - - def initWithValues_parent_(self, column_values, parent): - self.children = RowList() - self.update_values(column_values) - self.parent = parent - return self - - @staticmethod - def create_(values, parent): - return TreeNode.alloc().initWithValues_parent_(values, parent) - - def iterchildren(self): - return iter(self.children) - -class TreeTableModel(TableModelBase): - """https://develop.participatoryculture.org/index.php/WidgetAPITableView""" - def __init__(self, *column_values): - TableModelBase.__init__(self, *column_values) - self.iter_for_item = {} - - def containing_list(self, iter): - return self.row_list_for_iter(iter.value().parent) - - def row_list_for_iter(self, iter): - """Return the rows of all direct children of iter.""" - if iter is None: - return self.row_list - else: - return iter.value().children - - def remember_iter(self, iter): - self.iter_for_item[iter.value()] = iter - return iter - - def append(self, *column_values): - self.emit('structure-will-change') - retval = self.row_list.append(TreeNode.create_(column_values, None)) - return self.remember_iter(retval) - - def forget_iter_for_item(self, item): - del self.iter_for_item[item] - for child in item.children: - self.forget_iter_for_item(child) - - def remove(self, iter): - item = iter.value() - rv = TableModelBase.remove(self, iter) - self.forget_iter_for_item(item) - return rv - - def insert_before(self, iter, *column_values): - self.emit('structure-will-change') - row = TreeNode.create_(column_values, self.parent_iter(iter)) - retval = self.containing_list(iter).insertBefore(iter, row) - return self.remember_iter(retval) - - def append_child(self, iter, *column_values): - self.emit('structure-will-change') - row_list = self.row_list_for_iter(iter) - retval = row_list.append(TreeNode.create_(column_values, iter)) - return self.remember_iter(retval) - - def child_iter(self, iter): - row_list = iter.value().children - if len(row_list) == 0: - return None - else: - return row_list.firstIter() - - def nth_child_iter(self, iter, index): - row_list = self.row_list_for_iter(iter) - return row_list.nth_iter(index) - - def has_child(self, iter): - return len(iter.value().children) > 0 - - def children_count(self, iter): - if iter is not None: - return len(iter.value().children) - else: - return len(self.row_list) - - def children_iters(self, iter): - return self.iters_in_rowlist(self.row_list_for_iter(iter)) - - def parent_iter(self, iter): - return iter.value().parent - - def iter_for_row(self, tableview, row): - item = tableview.itemAtRow_(row) - if item in self.iter_for_item: - return self.iter_for_item[item] - elif item == -1: - raise WidgetActionError("no item at row %s" % row) - else: - raise WidgetActionError("no iter for item %s at row %s" % - (repr(item), row)) - - def row_of_iter(self, tableview, iter): - item = iter.value() - row = tableview.rowForItem_(item) - if row == -1: - raise LookupError("%s is not in this table" % repr(item)) - return row - - def get_path(self, iter_): - """Not implemented (yet?) for Cocoa. Currently the only place this is - needed is tablistmanager, where the situation that uses paths results - from GTK peculiarities. - """ - return NotImplemented - -class DataSourceBase(NSObject): - def initWithModel_(self, model): - self.model = model - self.drag_source = None - self.drag_dest = None - return self - - def setDragSource_(self, drag_source): - self.drag_source = drag_source - - def setDragDest_(self, drag_dest): - self.drag_dest = drag_dest - - def view_writeColumnData_ToPasteboard_(self, view, data, pasteboard): - if not self.drag_source: - return NO - wrapper = wrappermap.wrapper(view) - drag_data = self.drag_source.begin_drag(wrapper, data) - if not drag_data: - return NO - pasteboard.declareTypes_owner_((MIRO_DND_ITEM_LOCAL,), self) - for typ, value in drag_data.items(): - stringval = repr((repr(value), typ)) - pasteboard.setString_forType_(stringval, MIRO_DND_ITEM_LOCAL) - return YES - - def calcType_(self, drag_info): - source_actions = drag_info.draggingSourceOperationMask() - if not (self.drag_dest and - (self.drag_dest.allowed_actions() | source_actions)): - return None - types = self.drag_dest.allowed_types() - available = drag_info.draggingPasteboard().availableTypeFromArray_( - (MIRO_DND_ITEM_LOCAL,)) - if available: - # XXX using eval() sucks. - data = eval(drag_info.draggingPasteboard().stringForType_( - MIRO_DND_ITEM_LOCAL)) - if data: - _, typ = data - return typ - return None - - def validateDrop_dragInfo_parentIter_position_(self, view, drag_info, - parent, position): - typ = self.calcType_(drag_info) - if typ: - wrapper = wrappermap.wrapper(view) - drop_action = self.drag_dest.validate_drop( - wrapper, self.model, typ, - drag_info.draggingSourceOperationMask(), parent, - position) - if not drop_action: - return NSDragOperationNone - if isinstance(drop_action, (tuple, list)): - drop_action, iter = drop_action - view.setDropRow_dropOperation_( - self.model.row_of_iter(view, iter), - NSTableViewDropOn) - return drop_action - else: - return NSDragOperationNone - - def acceptDrop_dragInfo_parentIter_position_(self, view, drag_info, - parent, position): - typ = self.calcType_(drag_info) - if typ: - # XXX using eval sucks. - data = eval(drag_info.draggingPasteboard().stringForType_(MIRO_DND_ITEM_LOCAL)) - ids, _ = data - ids = eval(ids) - wrapper = wrappermap.wrapper(view) - self.drag_dest.accept_drop(wrapper, self.model, typ, - drag_info.draggingSourceOperationMask(), parent, - position, ids) - return YES - else: - return NO - -class MiroTableViewDataSource(DataSourceBase, protocols.NSTableDataSource): - def numberOfRowsInTableView_(self, table_view): - return len(self.model) - - def tableView_objectValueForTableColumn_row_(self, table_view, column, row): - node = self.model.nth_iter(row).value() - self.model.remember_row_at_index(node, row) - return self.model.get_column_data(node.values, column) - - def tableView_writeRowsWithIndexes_toPasteboard_(self, tableview, rowIndexes, - pasteboard): - indexes = list_from_nsindexset(rowIndexes) - data = [self.model[self.model.nth_iter(i)] for i in indexes] - return self.view_writeColumnData_ToPasteboard_(tableview, data, - pasteboard) - - def translateRow_operation_(self, row, operation): - if operation == NSTableViewDropOn: - return self.model.nth_iter(row), -1 - else: - return None, row - - def tableView_validateDrop_proposedRow_proposedDropOperation_(self, - tableview, drag_info, row, operation): - parent, position = self.translateRow_operation_(row, operation) - drop_action = self.validateDrop_dragInfo_parentIter_position_(tableview, - drag_info, parent, position) - if isinstance(drop_action, (list, tuple)): - # XXX nothing uses this yet - drop_action, iter = drop_action - tableview.setDropRow_dropOperation_( - self.model.row_of_iter(tableview, iter), - NSTableViewDropOn) - return drop_action - - def tableView_acceptDrop_row_dropOperation_(self, - tableview, drag_info, row, operation): - parent, position = self.translateRow_operation_(row, operation) - return self.acceptDrop_dragInfo_parentIter_position_(tableview, - drag_info, parent, position) - - -class MiroOutlineViewDataSource(DataSourceBase, protocols.NSOutlineViewDataSource): - def outlineView_child_ofItem_(self, view, child, item): - if item is nil: - row_list = self.model.row_list - else: - row_list = item.children - return row_list.nth_iter(child).value() - - def outlineView_isItemExpandable_(self, view, item): - if item is not nil and hasattr(item, 'children'): - return len(item.children) > 0 - else: - return len(self.model) > 0 - - def outlineView_numberOfChildrenOfItem_(self, view, item): - if item is not nil and hasattr(item, 'children'): - return len(item.children) - else: - return len(self.model) - - def outlineView_objectValueForTableColumn_byItem_(self, view, column, - item): - return self.model.get_column_data(item.values, column) - - def outlineView_writeItems_toPasteboard_(self, outline_view, items, - pasteboard): - data = [i.values for i in items] - return self.view_writeColumnData_ToPasteboard_(outline_view, data, - pasteboard) - - def outlineView_validateDrop_proposedItem_proposedChildIndex_(self, - outlineview, drag_info, item, child_index): - if item is None: - iter = None - else: - iter = self.model.iter_for_item[item] - drop_action = self.validateDrop_dragInfo_parentIter_position_( - outlineview, drag_info, iter, child_index) - if isinstance(drop_action, (tuple, list)): - drop_action, iter = drop_action - outlineview.setDropItem_dropChildIndex_( - iter.value(), NSOutlineViewDropOnItemIndex) - return drop_action - - def outlineView_acceptDrop_item_childIndex_(self, outlineview, drag_info, - item, child_index): - if item is None: - iter = None - else: - iter = self.model.iter_for_item[item] - return self.acceptDrop_dragInfo_parentIter_position_(outlineview, - drag_info, iter, child_index) diff --git a/mvc/widgets/osx/tableview.py b/mvc/widgets/osx/tableview.py deleted file mode 100644 index 2d2256f..0000000 --- a/mvc/widgets/osx/tableview.py +++ /dev/null @@ -1,1629 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -""".tableview -- TableView widget and it's -associated classes. -""" - -import math -import logging -from contextlib import contextmanager -from collections import namedtuple - -from AppKit import * -from Foundation import * -from objc import YES, NO, nil - -from mvc import signals -from mvc import errors -from mvc.widgets import widgetconst -from mvc.widgets.tableselection import SelectionOwnerMixin -from mvc.widgets.tablescroll import ScrollbarOwnerMixin -from .utils import filename_to_unicode -import wrappermap -import tablemodel -import osxmenus -from .base import Widget -from .simple import Image -from .drawing import DrawingContext, DrawingStyle, Gradient, ImageSurface -from .helpers import NotificationForwarder -from .layoutmanager import LayoutManager - -EXPANDER_PADDING = 6 -HEADER_HEIGHT = 17 -CUSTOM_HEADER_HEIGHT = 25 - -def iter_range(ns_range): - """Iterate over an NSRange object""" - return xrange(ns_range.location, ns_range.location + ns_range.length) - -Rect = namedtuple('Rect', 'x y width height') -def NSRectToRect(nsrect): - origin, size = nsrect.origin, nsrect.size - return Rect(origin.x, origin.y, size.width, size.height) - -Point = namedtuple('Point', 'x y') -def NSPointToPoint(nspoint): - return Point(int(nspoint.x), int(nspoint.y)) - -class HotspotTracker(object): - """Contains the info on the currently tracked hotspot. See: - https://develop.participatoryculture.org/index.php/WidgetAPITableView - """ - def __init__(self, tableview, point): - self.tableview = tableview - self.row = tableview.rowAtPoint_(point) - self.column = tableview.columnAtPoint_(point) - if self.row == -1 or self.column == -1: - self.hit = False - return - model = tableview.dataSource().model - self.iter = model.iter_for_row(tableview, self.row) - self.table_column = tableview.tableColumns()[self.column] - self.cell = self.table_column.dataCell() - self.update_position(point) - if isinstance(self.cell, CustomTableCell): - self.name = self.calc_hotspot() - else: - self.name = None - self.hit = (self.name is not None) - - def is_for_context_menu(self): - return self.name == '#show-context-menu' - - def calc_cell_hotspot(self, column, row): - if (self.hit and self.column == column and self.row == row): - return self.name - else: - return None - - def update_position(self, point): - cell_frame = self.tableview.frameOfCellAtColumn_row_(self.column, - self.row) - self.pos = NSPoint(point.x - cell_frame.origin.x, - point.y - cell_frame.origin.y) - - def update_hit(self): - old_hit = self.hit - self.hit = (self.calc_hotspot() == self.name) - if old_hit != self.hit: - self.redraw_cell() - - def set_cell_data(self): - model = self.tableview.dataSource().model - row = model[self.iter] - value_dict = model.get_column_data(row, self.table_column) - self.cell.setObjectValue_(value_dict) - self.cell.set_wrapper_data() - - def calc_hotspot(self): - self.set_cell_data() - cell_frame = self.tableview.frameOfCellAtColumn_row_(self.column, - self.row) - style = self.cell.make_drawing_style(cell_frame, self.tableview) - layout_manager = self.cell.layout_manager - layout_manager.reset() - return self.cell.wrapper.hotspot_test(style, layout_manager, - self.pos.x, self.pos.y, cell_frame.size.width, - cell_frame.size.height) - - def redraw_cell(self): - # Check to see if we removed the table in response to a hotspot click. - if self.tableview.superview() is not nil: - cell_frame = self.tableview.frameOfCellAtColumn_row_(self.column, - self.row) - self.tableview.setNeedsDisplayInRect_(cell_frame) - -def _calc_interior_frame(total_frame, tableview): - """Calculate the inner cell area for a table cell. - - We tell cocoa that the intercell spacing is (0, 0) and instead handle the - spacing ourselves. This method calculates the area that a cell should - render to, given the total spacing. - """ - return NSMakeRect(total_frame.origin.x + tableview.column_spacing // 2, - total_frame.origin.y + tableview.row_spacing // 2, - total_frame.size.width - tableview.column_spacing, - total_frame.size.height - tableview.row_spacing) - -class MiroTableCell(NSTextFieldCell): - def init(self): - return super(MiroTableCell, self).initTextCell_('') - - def calcHeight_(self, view): - font = self.font() - return math.ceil(font.ascender() + abs(font.descender()) + - font.leading()) - - def highlightColorWithFrame_inView_(self, frame, view): - return nil - - def setObjectValue_(self, value_dict): - if isinstance(value_dict, dict): - NSCell.setObjectValue_(self, value_dict['value']) - else: - # OS X calls setObjectValue_('') on intialization - NSCell.setObjectValue_(self, value_dict) - - def drawInteriorWithFrame_inView_(self, frame, view): - return NSTextFieldCell.drawInteriorWithFrame_inView_(self, - _calc_interior_frame(frame, view), view) - -class MiroTableInfoListTextCell(MiroTableCell): - def initWithAttrGetter_(self, attr_getter): - self = self.init() - self.setWraps_(NO) - self.attr_getter = attr_getter - self._textColor = self.textColor() - return self - - def drawWithFrame_inView_(self, frame, view): - # adjust frame based on the cell spacing - frame = _calc_interior_frame(frame, view) - if (self.isHighlighted() and frame is not None and - (view.isDescendantOf_(view.window().firstResponder()) or - view.gradientHighlight) and view.window().isMainWindow()): - self.setTextColor_(NSColor.whiteColor()) - else: - self.setTextColor_(self._textColor) - return MiroTableCell.drawWithFrame_inView_(self, frame, view) - - def titleRectForBounds_(self, rect): - frame = MiroTableCell.titleRectForBounds_(self, rect) - text_size = self.attributedStringValue().size() - frame.origin.y = rect.origin.y + (rect.size.height - text_size.height) / 2.0 - return frame - - def drawInteriorWithFrame_inView_(self, frame, view): - rect = self.titleRectForBounds_(frame) - self.attributedStringValue().drawInRect_(rect) - - def setObjectValue_(self, value): - if isinstance(value, tuple): - info, attrs, group_info = value - cell_text = self.attr_getter(info) - NSCell.setObjectValue_(self, cell_text) - else: - # Getting set to a something other than a model row, usually this - # happens in initialization - NSCell.setObjectValue_(self, '') - -class MiroTableImageCell(NSImageCell): - def calcHeight_(self, view): - return self.value_dict['image'].size().height - - def highlightColorWithFrame_inView_(self, frame, view): - return nil - - def setObjectValue_(self, value_dict): - NSImageCell.setObjectValue_(self, value_dict['image']) - - def drawInteriorWithFrame_inView_(self, frame, view): - return NSImageCell.drawInteriorWithFrame_inView_(self, - _calc_interior_frame(frame, view), view) - -class MiroCheckboxCell(NSButtonCell): - def init(self): - self = super(MiroCheckboxCell, self).init() - self.setButtonType_(NSSwitchButton) - self.setTitle_('') - return self - - def calcHeight_(self, view): - return self.cellSize().height - - def highlightColorWithFrame_inView_(self, frame, view): - return nil - - def setObjectValue_(self, value_dict): - if isinstance(value_dict, dict): - NSButtonCell.setObjectValue_(self, value_dict['value']) - else: - # OS X calls setObjectValue_('') on intialization - NSCell.setObjectValue_(self, value_dict) - - def startTrackingAt_inView_(self, point, view): - return YES - - def continueTracking_at_inView_(self, lastPoint, at, view): - return YES - - def stopTracking_at_inView_mouseIsUp_(self, lastPoint, at, tableview, mouseIsUp): - if mouseIsUp: - column = tableview.columnAtPoint_(at) - row = tableview.rowAtPoint_(at) - if column != -1 and row != -1: - wrapper = wrappermap.wrapper(tableview) - column = wrapper.columns[column] - itr = wrapper.model.iter_for_row(tableview, row) - column.renderer.emit('clicked', itr) - return NSButtonCell.stopTracking_at_inView_mouseIsUp_(self, lastPoint, - at, tableview, mouseIsUp) - - def drawInteriorWithFrame_inView_(self, frame, view): - return NSButtonCell.drawInteriorWithFrame_inView_(self, - _calc_interior_frame(frame, view), view) - -class CellRendererBase(object): - DRAW_BACKGROUND = True - - def set_index(self, index): - self.index = index - - def get_index(self): - return self.index - -class CellRenderer(CellRendererBase): - def __init__(self): - self.cell = self.build_cell() - self._font_scale_factor = 1.0 - self._font_bold = False - self.set_align('left') - - def build_cell(self): - return MiroTableCell.alloc().init() - - def setDataCell_(self, column): - column.setDataCell_(self.cell) - - def set_text_size(self, size): - if size == widgetconst.SIZE_NORMAL: - self._font_scale_factor = 1.0 - elif size == widgetconst.SIZE_SMALL: - # make the scale factor such so that the font size is 11.0 - self._font_scale_factor = 11.0 / NSFont.systemFontSize() - else: - raise ValueError("Unknown size: %s" % size) - self._set_font() - - def set_font_scale(self, scale_factor): - self._font_scale_factor = scale_factor - self._set_font() - - def set_bold(self, bold): - self._font_bold = bold - self._set_font() - - def _set_font(self): - size = NSFont.systemFontSize() * self._font_scale_factor - if self._font_bold: - font = NSFont.boldSystemFontOfSize_(size) - else: - font = NSFont.systemFontOfSize_(size) - self.cell.setFont_(font) - - def set_color(self, color): - color = NSColor.colorWithDeviceRed_green_blue_alpha_(color[0], - color[1], color[2], 1.0) - self.cell._textColor = color - self.cell.setTextColor_(color) - - def set_align(self, align): - if align == 'left': - ns_alignment = NSLeftTextAlignment - elif align == 'center': - ns_alignment = NSCenterTextAlignment - elif align == 'right': - ns_alignment = NSRightTextAlignment - else: - raise ValueError("unknown alignment: %s", align) - self.cell.setAlignment_(ns_alignment) - -class ImageCellRenderer(CellRendererBase): - def setDataCell_(self, column): - column.setDataCell_(MiroTableImageCell.alloc().init()) - -class CheckboxCellRenderer(CellRendererBase, signals.SignalEmitter): - def __init__(self): - signals.SignalEmitter.__init__(self, 'clicked') - self.size = widgetconst.SIZE_NORMAL - - def set_control_size(self, size): - self.size = size - - def setDataCell_(self, column): - cell = MiroCheckboxCell.alloc().init() - if self.size == widgetconst.SIZE_SMALL: - cell.setControlSize_(NSSmallControlSize) - column.setDataCell_(cell) - -class CustomTableCell(NSCell): - def init(self): - self = super(CustomTableCell, self).init() - self.layout_manager = LayoutManager() - self.hotspot = None - self.default_drawing_style = DrawingStyle() - return self - - def highlightColorWithFrame_inView_(self, frame, view): - return nil - - def calcHeight_(self, view): - self.layout_manager.reset() - self.set_wrapper_data() - cell_size = self.wrapper.get_size(self.default_drawing_style, - self.layout_manager) - return cell_size[1] - - def make_drawing_style(self, frame, view): - text_color = None - if (self.isHighlighted() and frame is not None and - (view.isDescendantOf_(view.window().firstResponder()) or - view.gradientHighlight) and view.window().isMainWindow()): - text_color = NSColor.whiteColor() - return DrawingStyle(text_color=text_color) - - def drawInteriorWithFrame_inView_(self, frame, view): - NSGraphicsContext.currentContext().saveGraphicsState() - if not self.wrapper.IGNORE_PADDING: - # adjust frame based on the cell spacing. We also have to adjust - # the hover position to account for the new frame - original_frame = frame - frame = _calc_interior_frame(frame, view) - hover_adjustment = (frame.origin.x - original_frame.origin.x, - frame.origin.y - original_frame.origin.y) - else: - hover_adjustment = (0, 0) - if self.wrapper.outline_column: - pad_left = EXPANDER_PADDING - else: - pad_left = 0 - drawing_rect = NSMakeRect(frame.origin.x + pad_left, frame.origin.y, - frame.size.width - pad_left, frame.size.height) - context = DrawingContext(view, drawing_rect, drawing_rect) - context.style = self.make_drawing_style(frame, view) - self.layout_manager.reset() - self.set_wrapper_data() - column = self.wrapper.get_index() - hover_pos = view.get_hover(self.row, column) - if hover_pos is not None: - hover_pos = [hover_pos[0] - hover_adjustment[0], - hover_pos[1] - hover_adjustment[1]] - self.wrapper.render(context, self.layout_manager, self.isHighlighted(), - self.hotspot, hover_pos) - NSGraphicsContext.currentContext().restoreGraphicsState() - - def setObjectValue_(self, value): - self.object_value = value - - def set_wrapper_data(self): - self.wrapper.__dict__.update(self.object_value) - -class CustomCellRenderer(CellRendererBase): - CellClass = CustomTableCell - - IGNORE_PADDING = False - - def __init__(self): - self.outline_column = False - self.index = None - - def setDataCell_(self, column): - # Note that the ownership is the opposite of what happens in widgets. - # The NSObject owns it's wrapper widget. This happens for a couple - # reasons: - # 1) The data cell gets copied a bunch of times, so wrappermap won't - # work with it. - # 2) The Wrapper should only needs to stay around as long as the - # NSCell that it's wrapping is around. Once the column gets removed - # from the table, the wrapper can be deleted. - nscell = self.CellClass.alloc().init() - nscell.wrapper = self - column.setDataCell_(nscell) - - def hotspot_test(self, style, layout, x, y, width, height): - return None - -class InfoListTableCell(CustomTableCell): - def set_wrapper_data(self): - self.wrapper.info, self.wrapper.attrs, self.wrapper.group_info = \ - self.object_value - -class InfoListRenderer(CustomCellRenderer): - CellClass = InfoListTableCell - - def hotspot_test(self, style, layout, x, y, width, height): - return None - -class InfoListRendererText(CellRenderer): - def build_cell(self): - cell = MiroTableInfoListTextCell.alloc() - return cell.initWithAttrGetter_(self.get_value) - -def calc_row_height(view, model_row): - max_height = 0 - model = view.dataSource().model - for column in view.tableColumns(): - cell = column.dataCell() - data = model.get_column_data(model_row, column) - cell.setObjectValue_(data) - cell_height = cell.calcHeight_(view) - max_height = max(max_height, cell_height) - if max_height == 0: - max_height = 12 - return max_height + view.row_spacing - -class TableViewDelegate(NSObject): - def tableView_willDisplayCell_forTableColumn_row_(self, view, cell, - column, row): - column = view.column_index_map[column] - cell.column = column - cell.row = row - if view.hotspot_tracker: - cell.hotspot = view.hotspot_tracker.calc_cell_hotspot(column, row) - else: - cell.hotspot = None - - def tableView_didClickTableColumn_(self, tableview, column): - wrapper = wrappermap.wrapper(tableview) - for column_wrapper in wrapper.columns: - if column_wrapper._column is column: - column_wrapper.emit('clicked') - - def tableView_toolTipForCell_rect_tableColumn_row_mouseLocation_(self, tableview, cell, rect, column, row, location): - wrapper = wrappermap.wrapper(tableview) - iter = tableview.dataSource().model.iter_for_row(tableview, row) - for wrapper_column in wrapper.columns: - if wrapper_column._column is column: - break - return (wrapper.get_tooltip(iter, wrapper_column), rect) - -class VariableHeightTableViewDelegate(TableViewDelegate): - def tableView_heightOfRow_(self, table_view, row): - model = table_view.dataSource().model - iter = model.iter_for_row(table_view, row) - if iter is None: - return 12 - return calc_row_height(table_view, model[iter]) - - -# TableViewCommon is a hack to do a Mixin class. We want the same behaviour -# for our table views and our outline views. Normally we would use a Mixin, -# but that doesn't work with pyobjc. Instead we define the common code in -# TableViewCommon, then copy it into MiroTableView and MiroOutlineView - -class TableViewCommon(object): - def init(self): - self = super(self.__class__, self).init() - self.hotspot_tracker = None - self._tracking_rects = [] - self.hover_info = None - self.column_index_map = {} - self.setFocusRingType_(NSFocusRingTypeNone) - self.handled_last_mouse_down = False - self.gradientHighlight = False - self.tracking_area = None - self.group_lines_enabled = False - self.group_line_width = 1 - self.group_line_color = (0, 0, 0, 1.0) - # we handle cell spacing manually - self.setIntercellSpacing_(NSSize(0, 0)) - self.column_spacing = 3 - self.row_spacing = 2 - return self - - def updateTrackingAreas(self): - # remove existing tracking area if needed - if self.tracking_area: - self.removeTrackingArea_(self.tracking_area) - - # create a new tracking area for the entire view. This allows us to - # get mouseMoved events whenever the mouse is inside our view. - self.tracking_area = NSTrackingArea.alloc() - self.tracking_area.initWithRect_options_owner_userInfo_( - self.visibleRect(), - NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | - NSTrackingActiveInKeyWindow, - self, - nil) - self.addTrackingArea_(self.tracking_area) - - def addTableColumn_(self, column): - index = len(self.tableColumns()) - column.set_index(index) - self.column_index_map[column._column] = index - self.SuperClass.addTableColumn_(self, column._column) - - def removeTableColumn_(self, column): - self.SuperClass.removeTableColumn_(self, column) - removed = self.column_index_map.pop(column) - for key, index in self.column_index_map.items(): - if index > removed: - self.column_index_map[key] -= 1 - - def moveColumn_toColumn_(self, src, dest): - # Need to switch the TableColumn objects too - columns = wrappermap.wrapper(self).columns - columns[src], columns[dest] = columns[dest], columns[src] - for index, column in enumerate(columns): - column.set_index(index) - self.SuperClass.moveColumn_toColumn_(self, src, dest) - - def highlightSelectionInClipRect_(self, rect): - if wrappermap.wrapper(self).draws_selection: - if not self.gradientHighlight: - return self.SuperClass.highlightSelectionInClipRect_(self, - rect) - context = NSGraphicsContext.currentContext() - focused = self.isDescendantOf_(self.window().firstResponder()) - for row in tablemodel.list_from_nsindexset(self.selectedRowIndexes()): - self.drawBackgroundGradient(context, focused, row) - - def setFrameSize_(self, size): - if size.height == 0: - size.height = 4 - self.SuperClass.setFrameSize_(self, size) - - def drawBackgroundGradient(self, context, focused, row): - widget = wrappermap.wrapper(self) - window = widget.get_window() - if window and window.is_active(): - if focused: - start_color = (0.588, 0.717, 0.843) - end_color = (0.416, 0.568, 0.713) - top_line_color = (0.416, 0.569, 0.714, 1.0) - bottom_line_color = (0.416, 0.569, 0.714, 1.0) - else: - start_color = (168 / 255.0, 188 / 255.0, 208 / 255.0) - end_color = (129 / 255.0, 152 / 255.0, 176 / 255.0) - top_line_color = (129 / 255.0, 152 / 255.0, 175 / 255.0, 1.0) - bottom_line_color = (0.416, 0.569, 0.714, 1.0) - else: - start_color = (0.675, 0.722, 0.765) - end_color = (0.592, 0.659, 0.710) - top_line_color = (0.596, 0.635, 0.671, 1.0) - bottom_line_color = (0.522, 0.576, 0.620, 1.0) - - rect = self.rectOfRow_(row) - top = NSMakeRect(rect.origin.x, rect.origin.y, rect.size.width, 1) - context.saveGraphicsState() - # draw the top line - NSColor.colorWithDeviceRed_green_blue_alpha_(*top_line_color).set() - NSRectFill(top) - bottom = NSMakeRect(rect.origin.x, rect.origin.y + rect.size.height - 2, - rect.size.width, 1) - NSColor.colorWithDeviceRed_green_blue_alpha_(*bottom_line_color).set() - NSRectFill(bottom) - highlight = NSMakeRect(rect.origin.x, rect.origin.y + rect.size.height - 1, - rect.size.width, 1) - NSColor.colorWithDeviceRed_green_blue_alpha_(0.918, 0.925, 0.941, 1.0).set() - NSRectFill(highlight) - - # draw the gradient - rect.origin.y += 1 - rect.size.height -= 3 - NSRectClip(rect) - gradient = Gradient(rect.origin.x, rect.origin.y, - rect.origin.x, rect.origin.y + rect.size.height) - gradient.set_start_color(start_color) - gradient.set_end_color(end_color) - gradient.draw() - context.restoreGraphicsState() - - def drawBackgroundInClipRect_(self, clip_rect): - # save our graphics state, since we are about to modify the clip path - graphics_context = NSGraphicsContext.currentContext() - graphics_context.saveGraphicsState() - # create a NSBezierPath that contains the rects of the columns with - # DRAW_BACKGROUND True. - clip_path = NSBezierPath.bezierPath() - number_of_columns = len(self.tableColumns()) - for col_index in iter_range(self.columnsInRect_(clip_rect)): - column = wrappermap.wrapper(self.tableColumns()[col_index]) - column_rect = None - if column.renderer.DRAW_BACKGROUND: - # We should draw the background for this column, add it's rect - # to our clip rect. - column_rect = self.rectOfColumn_(col_index) - clip_path.appendBezierPathWithRect_(column_rect) - else: - # We shouldn't draw the background for this column. Don't add - # anything to the clip rect, but do draw the area before the - # first row and after the last row. - self.drawBackgroundOutsideContent_clipRect_(col_index, - clip_rect) - if col_index == number_of_columns - 1: # last column - if not column_rect: - column_rect = self.rectOfColumn_(col_index) - column_right = column_rect.origin.x + column_rect.size.width - clip_right = clip_rect.origin.x + clip_rect.size.width - if column_right < clip_right: - # there's space to the right, so add that to the clip_rect - remaining = clip_right - column_right - left_rect = NSMakeRect(column_right, clip_rect.origin.y, - remaining, clip_rect.size.height) - clip_path.appendBezierPathWithRect_(left_rect) - # clip to that path - clip_path.addClip() - # do the default drawing - self.SuperClass.drawBackgroundInClipRect_(self, clip_rect) - # restore graphics state - graphics_context.restoreGraphicsState() - - def drawBackgroundOutsideContent_clipRect_(self, index, clip_rect): - """Draw our background outside the rows with content - - We call this for cells with DRAW_BACKGROUND set to False. For those, - we let the cell draw their own background, but we still need to draw - the background before the first cell and after the last cell. - """ - - self.backgroundColor().set() - - total_rect = NSIntersectionRect(self.rectOfColumn_(index), clip_rect) - - if self.numberOfRows() == 0: - # if no rows are selected, draw the background over everything - NSRectFill(total_rect) - return - - # fill the area above the first row - first_row_rect = self.rectOfRow_(0) - if first_row_rect.origin.y > total_rect.origin.y: - height = first_row_rect.origin.y - total_rect.origin.y - NSRectFill(NSMakeRect(total_rect.origin.x, total_rect.origin.y, - total_rect.size.width, height)) - - # fill the area below the last row - last_row_rect = self.rectOfRow_(self.numberOfRows()-1) - if NSMaxY(last_row_rect) < NSMaxY(total_rect): - y = NSMaxY(last_row_rect) + 1 - height = NSMaxY(total_rect) - NSMaxY(last_row_rect) - NSRectFill(NSMakeRect(total_rect.origin.x, y, - total_rect.size.width, height)) - - def drawRow_clipRect_(self, row, clip_rect): - self.SuperClass.drawRow_clipRect_(self, row, clip_rect) - if self.group_lines_enabled: - self.drawGroupLine_(row) - - def drawGroupLine_(self, row): - infolist = wrappermap.wrapper(self).model - if (not isinstance(infolist, tablemodel.InfoListModel) or - infolist.get_grouping() is None): - return - - info, attrs, group_info = infolist[row] - if group_info[0] == group_info[1] - 1: - rect = self.rectOfRow_(row) - rect.origin.y = NSMaxY(rect) - self.group_line_width - rect.size.height = self.group_line_width - NSColor.colorWithDeviceRed_green_blue_alpha_( - *self.group_line_color).set() - NSRectFill(rect) - - def canDragRowsWithIndexes_atPoint_(self, indexes, point): - return YES - - def draggingSourceOperationMaskForLocal_(self, local): - drag_source = wrappermap.wrapper(self).drag_source - if drag_source and local: - return drag_source.allowed_actions() - return NSDragOperationNone - - def mouseMoved_(self, event): - location = self.convertPoint_fromView_(event.locationInWindow(), nil) - row = self.rowAtPoint_(location) - column = self.columnAtPoint_(location) - if (self.hover_info is not None and self.hover_info != (row, column)): - # left a cell, redraw it the old one - rect = self.frameOfCellAtColumn_row_(self.hover_info[1], - self.hover_info[0]) - self.setNeedsDisplayInRect_(rect) - if row == -1 or column == -1: - # corner case: we got a mouseMoved_ event, but the pointer is - # outside the view - self.hover_pos = self.hover_info = None - return - # queue a redraw on the cell currently hovered over - rect = self.frameOfCellAtColumn_row_(column, row) - self.setNeedsDisplayInRect_(rect) - # recalculate hover_pos and hover_info - self.hover_pos = (location[0] - rect[0][0], - location[0] - rect[0][1]) - self.hover_info = (row, column) - - def mouseExited_(self, event): - if self.hover_info: - # mouse left our window, unset hover and redraw the cell that the - # mouse was in - rect = self.frameOfCellAtColumn_row_(self.hover_info[1], - self.hover_info[0]) - self.setNeedsDisplayInRect_(rect) - self.hover_pos = self.hover_info = None - - def get_hover(self, row, column): - if self.hover_info == (row, column): - return self.hover_pos - else: - return None - - def mouseDown_(self, event): - if event.modifierFlags() & NSControlKeyMask: - self.handleContextMenu_(event) - self.handled_last_mouse_down = True - return - - point = self.convertPoint_fromView_(event.locationInWindow(), nil) - - if event.clickCount() == 2: - if self.handled_last_mouse_down: - return - wrapper = wrappermap.wrapper(self) - row = self.rowAtPoint_(point) - if (row != -1 and self.point_should_click(point, row)): - iter = wrapper.model.iter_for_row(self, row) - wrapper.emit('row-activated', iter) - return - - # Like clickCount() == 2 but keep running so we can get to run the - # hotspot tracker et al. - if event.clickCount() == 1: - wrapper = wrappermap.wrapper(self) - row = self.rowAtPoint_(point) - if (row != -1 and self.point_should_click(point, row)): - - iter = wrapper.model.iter_for_row(self, row) - wrapper.emit('row-clicked', iter) - - hotspot_tracker = HotspotTracker(self, point) - if hotspot_tracker.hit: - self.hotspot_tracker = hotspot_tracker - self.hotspot_tracker.redraw_cell() - self.handled_last_mouse_down = True - if hotspot_tracker.is_for_context_menu(): - self.popup_context_menu(self.hotspot_tracker.row, event) - # once we're out of that call, we know the context menu is - # gone - hotspot_tracker.redraw_cell() - self.hotspot_tracker = None - else: - self.handled_last_mouse_down = False - self.SuperClass.mouseDown_(self, event) - - def point_should_click(self, point, row): - """Should a click on a point result in a row-clicked signal? - - Subclasses can override if not every point should result in a click. - """ - return True - - def rightMouseDown_(self, event): - self.handleContextMenu_(event) - - def handleContextMenu_(self, event): - self.window().makeFirstResponder_(self) - point = self.convertPoint_fromView_(event.locationInWindow(), nil) - column = self.columnAtPoint_(point) - row = self.rowAtPoint_(point) - if self.group_lines_enabled and column == 0: - self.selectAllItemsInGroupForRow_(row) - self.popup_context_menu(row, event) - - def selectAllItemsInGroupForRow_(self, row): - wrapper = wrappermap.wrapper(self) - infolist = wrapper.model - if (not isinstance(infolist, tablemodel.InfoListModel) or - infolist.get_grouping() is None): - return - - info, attrs, group_info = infolist[row] - select_range = NSMakeRange(row - group_info[0], group_info[1]) - index_set = NSIndexSet.indexSetWithIndexesInRange_(select_range) - self.selectRowIndexes_byExtendingSelection_(index_set, NO) - - def popup_context_menu(self, row, event): - selection = self.selectedRowIndexes() - if row != -1 and not selection.containsIndex_(row): - index_set = NSIndexSet.alloc().initWithIndex_(row) - self.selectRowIndexes_byExtendingSelection_(index_set, NO) - wrapper = wrappermap.wrapper(self) - if wrapper.context_menu_callback is not None: - menu_items = wrapper.context_menu_callback(wrapper) - menu = osxmenus.make_context_menu(menu_items) - NSMenu.popUpContextMenu_withEvent_forView_(menu, event, self) - - def mouseDragged_(self, event): - if self.hotspot_tracker is not None: - point = self.convertPoint_fromView_(event.locationInWindow(), nil) - self.hotspot_tracker.update_position(point) - self.hotspot_tracker.update_hit() - else: - self.SuperClass.mouseDragged_(self, event) - - def mouseUp_(self, event): - if self.hotspot_tracker is not None: - point = self.convertPoint_fromView_(event.locationInWindow(), nil) - self.hotspot_tracker.update_position(point) - self.hotspot_tracker.update_hit() - if self.hotspot_tracker.hit: - wrappermap.wrapper(self).send_hotspot_clicked() - if self.hotspot_tracker: - self.hotspot_tracker.redraw_cell() - self.hotspot_tracker = None - else: - self.SuperClass.mouseUp_(self, event) - - def keyDown_(self, event): - mods = osxmenus.translate_event_modifiers(event) - if event.charactersIgnoringModifiers() == ' ' and len(mods) == 0: - # handle spacebar with no modifiers by sending the row-activated - # signal - wrapper = wrappermap.wrapper(self) - row = self.selectedRow() - if row >= 0: - iter = wrapper.model.iter_for_row(self, row) - wrapper.emit('row-activated', iter) - else: - self.SuperClass.keyDown_(self, event) - -class TableColumn(signals.SignalEmitter): - def __init__(self, title, renderer, header=None, **attrs): - signals.SignalEmitter.__init__(self) - self.create_signal('clicked') - self._column = MiroTableColumn.alloc().initWithIdentifier_(title) - self._column.set_attrs(attrs) - wrappermap.add(self._column, self) - header_cell = MiroTableHeaderCell.alloc().init() - self.custom_header = False - if header: - header_cell.set_widget(header) - self.custom_header = True - self._column.setHeaderCell_(header_cell) - self._column.headerCell().setStringValue_(title) - self._column.setEditable_(NO) - self._column.setResizingMask_(NSTableColumnNoResizing) - self.renderer = renderer - self.sort_order_ascending = True - self.sort_indicator_visible = False - self.do_horizontal_padding = True - self.min_width = self.max_width = None - renderer.setDataCell_(self._column) - - def set_do_horizontal_padding(self, horizontal_padding): - self.do_horizontal_padding = horizontal_padding - - def set_right_aligned(self, right_aligned): - if right_aligned: - self._column.headerCell().setAlignment_(NSRightTextAlignment) - else: - self._column.headerCell().setAlignment_(NSLeftTextAlignment) - - def set_min_width(self, width): - self.min_width = width - - def set_max_width(self, width): - self.max_width = width - - def set_width(self, width): - self._column.setWidth_(width) - - def get_width(self): - return self._column.width() - - def set_resizable(self, resizable): - mask = 0 - if resizable: - mask |= NSTableColumnUserResizingMask - self._column.setResizingMask_(mask) - - def set_sort_indicator_visible(self, visible): - self.sort_indicator_visible = visible - self._column.tableView().headerView().setNeedsDisplay_(True) - - def get_sort_indicator_visible(self): - return self.sort_indicator_visible - - def set_sort_order(self, ascending): - self.sort_order_ascending = ascending - self._column.tableView().headerView().setNeedsDisplay_(True) - - def get_sort_order_ascending(self): - return self.sort_order_ascending - - def set_index(self, index): - self.index = index - self.renderer.set_index(index) - -class MiroTableColumn(NSTableColumn): - def set_attrs(self, attrs): - self._attrs = attrs - - def attrs(self): - return self._attrs - -class MiroTableView(NSTableView): - SuperClass = NSTableView - for name, value in TableViewCommon.__dict__.items(): - locals()[name] = value - -class MiroTableHeaderView(NSTableHeaderView): - def initWithFrame_(self, frame): - # frame is not used - self = super(MiroTableHeaderView, self).initWithFrame_(frame) - self.selected = None - self.custom_header = False - return self - - def drawRect_(self, rect): - wrapper = wrappermap.wrapper(self.tableView()) - if self.selected: - self.selected.set_selected(False) - for column in wrapper.columns: - if column.sort_indicator_visible: - self.selected = column._column.headerCell() - self.selected.set_selected(True) - self.selected.set_ascending(column.sort_order_ascending) - break - NSTableHeaderView.drawRect_(self, rect) - if self.custom_header: - NSGraphicsContext.currentContext().saveGraphicsState() - # Draw the separator between the header and the contents. - context = DrawingContext(self, rect, rect) - context.set_line_width(1) - context.set_color((2 / 255.0, 2 / 255.0, 2 / 255.0)) - context.move_to(0, context.height - 0.5) - context.rel_line_to(context.width, 0) - context.stroke() - NSGraphicsContext.currentContext().restoreGraphicsState() - -class MiroTableHeaderCell(NSTableHeaderCell): - def init(self): - self = super(MiroTableHeaderCell, self).init() - self.layout_manager = LayoutManager() - self.button = None - return self - - def set_selected(self, selected): - self.button._enabled = selected - - def set_ascending(self, ascending): - self.button._ascending = ascending - - def set_widget(self, widget): - self.button = widget - - def drawWithFrame_inView_(self, frame, view): - if self.button is None: - # use the default behavior when set_widget hasn't been called - return NSTableHeaderCell.drawWithFrame_inView_(self, frame, view) - - NSGraphicsContext.currentContext().saveGraphicsState() - drawing_rect = NSMakeRect(frame.origin.x, frame.origin.y, - frame.size.width, frame.size.height) - context = DrawingContext(view, drawing_rect, drawing_rect) - context.style = self.make_drawing_style(frame, view) - self.layout_manager.reset() - columns = wrappermap.wrapper(view.tableView()).columns - header_cells = [c._column.headerCell() for c in columns] - background_only = not self in header_cells - self.button.draw(context, self.layout_manager, background_only) - NSGraphicsContext.currentContext().restoreGraphicsState() - - def make_drawing_style(self, frame, view): - text_color = None - if (self.isHighlighted() and frame is not None and - (view.isDescendantOf_(view.window().firstResponder()) or - view.gradientHighlight)): - text_color = NSColor.whiteColor() - return DrawingStyle(text_color=text_color) - -class CocoaSelectionOwnerMixin(SelectionOwnerMixin): - """Cocoa-specific methods for selection management. - - This subclass should not define any behavior. Methods that cannot be - completed in this widget state should raise WidgetActionError. - """ - - def _set_allow_multiple_select(self, allow): - self.tableview.setAllowsMultipleSelection_(allow) - - def _get_allow_multiple_select(self): - return self.tableview.allowsMultipleSelection() - - def _get_selected_iters(self): - selection = self.tableview.selectedRowIndexes() - selrows = tablemodel.list_from_nsindexset(selection) - return [self.model.iter_for_row(self.tableview, row) for row in selrows] - - def _get_selected_iter(self): - row = self.tableview.selectedRow() - if row == -1: - return None - return self.model.iter_for_row(self.tableview, row) - - def _get_selected_rows(self): - return [self.model[i] for i in self._get_selected_iters()] - - @property - def num_rows_selected(self): - return self.tableview.numberOfSelectedRows() - - def _is_selected(self, iter_): - row = self.row_of_iter(iter_) - return self.tableview.isRowSelected_(row) - - def _select(self, iter_): - row = self.row_of_iter(iter_) - index_set = NSIndexSet.alloc().initWithIndex_(row) - self.tableview.selectRowIndexes_byExtendingSelection_(index_set, YES) - - def _unselect(self, iter_): - self.tableview.deselectRow_(self.row_of_iter(iter_)) - - def _unselect_all(self): - self.tableview.deselectAll_(nil) - - def _iter_to_string(self, iter_): - return unicode(self.model.row_of_iter(self.tableview, iter_)) - - def _iter_from_string(self, row): - return self.model.iter_for_row(self.tableview, int(row)) - -class CocoaScrollbarOwnerMixin(ScrollbarOwnerMixin): - """Manages a TableView's scroll position.""" - def __init__(self): - ScrollbarOwnerMixin.__init__(self, _work_around_17153=True) - self.connect('place-in-scroller', self.on_place_in_scroller) - self.scroll_position = (0, 0) - self.clipview_notifications = None - self._position_set = False - - def _set_scroll_position(self, scroll_to): - """Restore a saved scroll position.""" - self.scroll_position = scroll_to - try: - scroller = self._scroller - except errors.WidgetNotReadyError: - return - self._position_set = True - clipview = scroller.contentView() - if not self.clipview_notifications: - self.clipview_notifications = NotificationForwarder.create(clipview) - # NOTE: intentional changes are BoundsChanged; bad changes are - # FrameChanged - clipview.setPostsFrameChangedNotifications_(YES) - self.clipview_notifications.connect(self.on_scroll_changed, - 'NSViewFrameDidChangeNotification') - # NOTE: scrollPoint_ just scrolls the point into view; we want to - # scroll the view so that the point becomes the origin - size = self.tableview.visibleRect().size - size = (size.width, size.height) - rect = NSMakeRect(scroll_to[0], scroll_to[1], size[0], size[1]) - self.tableview.scrollRectToVisible_(rect) - - def on_place_in_scroller(self, scroller): - # workaround for 17153.1 - if not self._position_set: - self._set_scroll_position(self.scroll_position) - - @property - def _manually_scrolled(self): - """Return whether the view has been scrolled explicitly by the user - since the last time it was set automatically. Ignores X coords. - """ - auto_y = self.scroll_position[1] - real_y = self.get_scroll_position()[1] - return abs(auto_y - real_y) > 5 - - def _get_item_area(self, iter_): - rect = self.tableview.rectOfRow_(self.row_of_iter(iter_)) - return NSRectToRect(rect) - - def _get_visible_area(self): - return NSRectToRect(self._scroller.contentView().documentVisibleRect()) - - def _get_scroll_position(self): - point = self._scroller.contentView().documentVisibleRect().origin - return NSPointToPoint(point) - - def on_scroll_changed(self, notification): - # we get this notification when the scroll position has been reset (when - # it should not have been); put it back - self.set_scroll_position(self.scroll_position) - # this notification also serves as the Cocoa equivalent to - # on_scroll_range_changed, which tells super when we may be ready to - # scroll to an iter - self.emit('scroll-range-changed') - - def set_scroller(self, scroller): - """For GTK; Cocoa tableview knows its enclosingScrollView""" - - @property - def _scroller(self): - """Return an NSScrollView or raise WidgetNotReadyError""" - scroller = self.tableview.enclosingScrollView() - if not scroller: - raise errors.WidgetNotReadyError('enclosingScrollView') - return scroller - -class SorterPadding(NSView): - # Why is this a Mac only widget? Because the wrappermap mechanism requires - # us to layout the widgets (so that we may call back to the portable API - # hooks of the widget. Since we only set the view component, this fake - # widget is never placed so the wrappermap mechanism fails to work. - # - # So far, this is okay because only the Mac uses custom headers. - def init(self): - self = super(SorterPadding, self).init() - image = Image(resources.path('images/headertoolbar.png')) - self.image = ImageSurface(image) - return self - - def isFlipped(self): - return YES - - def drawRect_(self, rect): - context = DrawingContext(self, self.bounds(), rect) - context.style = DrawingStyle() - self.image.draw(context, 0, 0, context.width, context.height) - # XXX this color doesn't take into account enable/disabled state - # of the sorting widgets. - edge = 72.0 / 255 - context.set_color((edge, edge, edge)) - context.set_line_width(1) - context.move_to(0.5, 0) - context.rel_line_to(0, context.height) - context.stroke() - -class TableView(CocoaSelectionOwnerMixin, CocoaScrollbarOwnerMixin, Widget): - """Displays data as a tabular list. TableView follows the GTK TreeView - widget fairly closely. - """ - - CREATES_VIEW = False - # Bit of a hack. We create several views. By setting CREATES_VIEW to - # False, we get to position the views manually. - - draws_selection = True - - def __init__(self, model, custom_headers=False): - Widget.__init__(self) - CocoaSelectionOwnerMixin.__init__(self) - CocoaScrollbarOwnerMixin.__init__(self) - self.create_signal('hotspot-clicked') - self.create_signal('row-clicked') - self.create_signal('row-activated') - self.create_signal('reallocate-columns') - self.model = model - self.columns = [] - self.drag_source = self.drag_dest = None - self.context_menu_callback = None - self.tableview = MiroTableView.alloc().init() - self.data_source = tablemodel.MiroTableViewDataSource.alloc() - types = (tablemodel.MIRO_DND_ITEM_LOCAL,) - self.tableview.registerForDraggedTypes_(types) - self.view = self.tableview - self.data_source.initWithModel_(self.model) - self.tableview.setDataSource_(self.data_source) - self.tableview.setVerticalMotionCanBeginDrag_(YES) - self.set_columns_draggable(False) - self.set_auto_resizes(False) - self.row_height_set = False - self.set_fixed_height(False) - self.auto_resizing = False - self.header_view = MiroTableHeaderView.alloc().initWithFrame_( - NSMakeRect(0, 0, 0, 0)) - self.tableview.setCornerView_(None) - self.custom_header = False - self.header_height = HEADER_HEIGHT - self.set_show_headers(True) - self.notifications = NotificationForwarder.create(self.tableview) - self.model_signal_ids = [ - self.model.connect_weak('row-changed', self.on_row_change), - self.model.connect_weak('structure-will-change', - self.on_model_structure_change), - ] - self.iters_to_update = [] - self.height_changed = self.reload_needed = False - self.old_selection = None - self._resizing = False - if custom_headers: - self._enable_custom_headers() - - def unset_model(self): - for signal_id in self.model_signal_ids: - self.model.disconnect(signal_id) - self.model = None - self.tableview.setDataSource_(None) - self.data_source = None - - def _enable_custom_headers(self): - self.custom_header = True - self.header_height = CUSTOM_HEADER_HEIGHT - self.header_view.custom_header = True - self.tableview.setCornerView_(SorterPadding.alloc().init()) - - def enable_album_view_focus_hack(self): - # this only matters on GTK - pass - - def focus(self): - if self.tableview.window() is not None: - self.tableview.window().makeFirstResponder_(self.tableview) - - def send_hotspot_clicked(self): - tracker = self.tableview.hotspot_tracker - self.emit('hotspot-clicked', tracker.name, tracker.iter) - - def on_row_change(self, model, iter): - self.iters_to_update.append(iter) - if not self.fixed_height: - self.height_changed = True - if self.tableview.hotspot_tracker is not None: - self.tableview.hotspot_tracker.update_hit() - - def on_model_structure_change(self, model): - self.will_need_reload() - self.cancel_hotspot_track() - - def will_need_reload(self): - if not self.reload_needed: - self.reload_needed = True - self.old_selection = self._get_selected_rows() - - def cancel_hotspot_track(self): - if self.tableview.hotspot_tracker is not None: - self.tableview.hotspot_tracker.redraw_cell() - self.tableview.hotspot_tracker = None - - def on_expanded(self, notification): - self.invalidate_size_request() - item = notification.userInfo()['NSObject'] - iter_ = self.model.iter_for_item[item] - self.emit('row-expanded', iter_, self.model.get_path(iter_)) - - def on_collapsed(self, notification): - self.invalidate_size_request() - item = notification.userInfo()['NSObject'] - iter_ = self.model.iter_for_item[item] - self.emit('row-collapsed', iter_, self.model.get_path(iter_)) - - def on_column_resize(self, notification): - if self.auto_resizing or self._resizing: - return - self._resizing = True - try: - column = notification.userInfo()['NSTableColumn'] - label = column.headerCell().stringValue() - self.emit('reallocate-columns', {label: column.width()}) - finally: - self._resizing = False - - def is_tree(self): - return isinstance(self.model, tablemodel.TreeTableModel) - - def set_row_expanded(self, iter, expanded): - """Expand or collapse the specified row. Succeeds or raises - WidgetActionError. - """ - item = iter.value() - if expanded: - self.tableview.expandItem_(item) - else: - self.tableview.collapseItem_(item) - if self.tableview.isItemExpanded_(item) != expanded: - raise errors.WidgetActionError( - "cannot expand iter. expandable: %r" % ( - self.tableview.isExpandable_(item),)) - self.invalidate_size_request() - - def is_row_expanded(self, iter): - return self.tableview.isItemExpanded_(iter.value()) - - def calc_size_request(self): - self.tableview.tile() - height = self.tableview.frame().size.height - if self._show_headers: - height += self.header_height - return self.calc_width(), height - - def viewport_repositioned(self): - self._do_layout() - - def viewport_created(self): - wrappermap.add(self.tableview, self) - self._do_layout() - self._add_views() - self.notifications.connect(self.on_selection_changed, - 'NSTableViewSelectionDidChangeNotification') - self.notifications.connect(self.on_column_resize, - 'NSTableViewColumnDidResizeNotification') - # scroll has been unset - self._position_set = False - - def remove_viewport(self): - if self.viewport is not None: - self._remove_views() - wrappermap.remove(self.tableview) - self.notifications.disconnect() - self.viewport = None - if self.clipview_notifications: - self.clipview_notifications.disconnect() - self.clipview_notifications = None - - def _should_place_header_view(self): - return self._show_headers and not self.parent_is_scroller - - def _add_views(self): - self.viewport.view.addSubview_(self.tableview) - if self._should_place_header_view(): - self.viewport.view.addSubview_(self.header_view) - - def _remove_views(self): - self.tableview.removeFromSuperview() - self.header_view.removeFromSuperview() - - def _do_layout(self): - x = self.viewport.placement.origin.x - y = self.viewport.placement.origin.y - width = self.viewport.get_width() - height = self.viewport.get_height() - if self._should_place_header_view(): - self.header_view.setFrame_(NSMakeRect(x, y, - width, self.header_height)) - self.tableview.setFrame_(NSMakeRect(x, y + self.header_height, - width, height - self.header_height)) - else: - self.header_view.setFrame_(NSMakeRect(x, y, - width, self.header_height)) - self.tableview.setFrame_(NSMakeRect(x, y, width, height)) - - if self.auto_resize: - self.auto_resizing = True - # ListView sizes itself in do_size_allocated; - # this is necessary for tablist and StandardView - columns = self.tableview.tableColumns() - if len(columns) == 1: - columns[0].setWidth_(self.viewport.area().size.width) - self.auto_resizing = False - self.queue_redraw() - - def calc_width(self): - if self.column_count() == 0: - return 0 - width = 0 - columns = self.tableview.tableColumns() - if self.auto_resize: - # Table auto-resizes, we can shrink to min-width for each column - width = sum(column.minWidth() for column in columns) - width += self.tableview.column_spacing * self.column_count() - else: - # Table doesn't auto-resize, the columns can't get smaller than - # their current width - width = sum(column.width() for column in columns) - return width - - def start_bulk_change(self): - # stop our model from emitting signals, which is slow if we're - # adding/removing/changing a bunch of rows. Instead, just reload the - # model afterwards. - self.will_need_reload() - self.cancel_hotspot_track() - self.model.freeze_signals() - - def model_changed(self): - if not self.row_height_set and self.fixed_height: - self.try_to_set_row_height() - self.model.thaw_signals() - size_changed = False - if self.reload_needed: - self.tableview.reloadData() - new_selection = self._get_selected_rows() - if new_selection != self.old_selection: - self.on_selection_changed(self.tableview) - self.old_selection = None - size_changed = True - elif self.iters_to_update: - if self.fixed_height or not self.height_changed: - # our rows don't change height, just update cell areas - if self.is_tree(): - for iter in self.iters_to_update: - self.tableview.reloadItem_(iter.value()) - else: - for iter in self.iters_to_update: - row = self.row_of_iter(iter) - rect = self.tableview.rectOfRow_(row) - self.tableview.setNeedsDisplayInRect_(rect) - else: - # our rows can change height inform Cocoa that their heights - # might have changed (this will redraw them) - index_set = NSMutableIndexSet.alloc().init() - for iter in self.iters_to_update: - try: - index_set.addIndex_(self.row_of_iter(iter)) - except LookupError: - # This happens when the iter's parent is unexpanded, - # just ignore. - pass - self.tableview.noteHeightOfRowsWithIndexesChanged_(index_set) - size_changed = True - else: - return - if size_changed: - self.invalidate_size_request() - self.height_changed = self.reload_needed = False - self.iters_to_update = [] - - def width_for_columns(self, width): - """If the table is width pixels big, how much width is available for - the table's columns. - """ - # XXX this used to do some calculation with the spacing of each column, - # but it doesn't appear like we need it to be that complicated anymore - # (see #18273) - return width - 2 - - def set_column_spacing(self, column_spacing): - self.tableview.column_spacing = column_spacing - - def set_row_spacing(self, row_spacing): - self.tableview.row_spacing = row_spacing - - def set_alternate_row_backgrounds(self, setting): - self.tableview.setUsesAlternatingRowBackgroundColors_(setting) - - def set_grid_lines(self, horizontal, vertical): - mask = 0 - if horizontal: - mask |= NSTableViewSolidHorizontalGridLineMask - if vertical: - mask |= NSTableViewSolidVerticalGridLineMask - self.tableview.setGridStyleMask_(mask) - - def set_gradient_highlight(self, setting): - self.tableview.gradientHighlight = setting - - def set_group_lines_enabled(self, enabled): - self.tableview.group_lines_enabled = enabled - self.queue_redraw() - - def set_group_line_style(self, color, width): - self.tableview.group_line_color = color + (1.0,) - self.tableview.group_line_width = width - self.queue_redraw() - - def get_tooltip(self, iter, column): - return None - - def add_column(self, column): - if not self.custom_header == column.custom_header: - raise ValueError('Column header does not match type ' - 'required by TableView') - self.columns.append(column) - self.tableview.addTableColumn_(column) - self._set_min_max_column_widths(column) - # Adding a column means that each row could have a different height. - # call noteNumberOfRowsChanged() to have OS X recalculate the heights - self.tableview.noteNumberOfRowsChanged() - self.invalidate_size_request() - self.try_to_set_row_height() - - def _set_min_max_column_widths(self, column): - if column.do_horizontal_padding: - spacing = self.tableview.column_spacing - else: - spacing = 0 - if column.min_width > 0: - column._column.setMinWidth_(column.min_width + spacing) - if column.max_width > 0: - column._column.setMaxWidth_(column.max_width + spacing) - - def column_count(self): - return len(self.tableview.tableColumns()) - - def remove_column(self, index): - column = self.columns.pop(index) - self.tableview.removeTableColumn_(column._column) - self.invalidate_size_request() - - def get_columns(self): - titles = [] - columns = self.tableview.tableColumns() - for column in columns: - titles.append(column.headerCell().stringValue()) - return titles - - def set_background_color(self, (red, green, blue)): - color = NSColor.colorWithDeviceRed_green_blue_alpha_(red, green, blue, - 1.0) - self.tableview.setBackgroundColor_(color) - - def set_show_headers(self, show): - self._show_headers = show - if show: - self.tableview.setHeaderView_(self.header_view) - else: - self.tableview.setHeaderView_(None) - if self.viewport is not None: - self._remove_views() - self._do_layout() - self._add_views() - self.invalidate_size_request() - self.queue_redraw() - - def is_showing_headers(self): - return self._show_headers - - def set_search_column(self, model_index): - pass - - def try_to_set_row_height(self): - if len(self.model) > 0: - first_iter = self.model.first_iter() - height = calc_row_height(self.tableview, self.model[first_iter]) - self.tableview.setRowHeight_(height) - self.row_height_set = True - - def set_auto_resizes(self, setting): - self.auto_resize = setting - - def set_columns_draggable(self, dragable): - self.tableview.setAllowsColumnReordering_(dragable) - - def set_fixed_height(self, fixed): - if fixed: - self.fixed_height = True - delegate_class = TableViewDelegate - self.row_height_set = False - self.try_to_set_row_height() - else: - self.fixed_height = False - delegate_class = VariableHeightTableViewDelegate - self.delegate = delegate_class.alloc().init() - self.tableview.setDelegate_(self.delegate) - self.tableview.reloadData() - - def row_of_iter(self, iter): - return self.model.row_of_iter(self.tableview, iter) - - def set_context_menu_callback(self, callback): - self.context_menu_callback = callback - - # disable the drag when the cells are constantly updating. Mac OS X - # deals badly with this.. - def set_volatile(self, volatile): - if volatile: - self.data_source.setDragSource_(None) - self.data_source.setDragDest_(None) - else: - self.data_source.setDragSource_(self.drag_source) - self.data_source.setDragDest_(self.drag_dest) - - def set_drag_source(self, drag_source): - self.drag_source = drag_source - self.data_source.setDragSource_(drag_source) - - def set_drag_dest(self, drag_dest): - self.drag_dest = drag_dest - if drag_dest is None: - self.data_source.setDragDest_(None) - else: - types = drag_dest.allowed_types() - self.data_source.setDragDest_(drag_dest) diff --git a/mvc/widgets/osx/utils.py b/mvc/widgets/osx/utils.py deleted file mode 100644 index c0c2d85..0000000 --- a/mvc/widgets/osx/utils.py +++ /dev/null @@ -1,2 +0,0 @@ -def filename_to_unicode(filename): - return filename.decode('utf8') diff --git a/mvc/widgets/osx/viewport.py b/mvc/widgets/osx/viewport.py deleted file mode 100644 index e6564d4..0000000 --- a/mvc/widgets/osx/viewport.py +++ /dev/null @@ -1,101 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -""".viewport.py -- Viewport classes - -A Viewport represents the area where a Widget is located. -""" - -from objc import YES, NO, nil -from Foundation import * - -class Viewport(object): - """Used when a widget creates it's own NSView.""" - def __init__(self, view, initial_frame): - self.view = view - self.view.setFrame_(initial_frame) - self.placement = initial_frame - - def at_position(self, rect): - """Check if a viewport is currently positioned at rect.""" - return self.placement == rect - - def reposition(self, rect): - """Move the viewport to a different position.""" - self.view.setFrame_(rect) - self.placement = rect - - def remove(self): - self.view.removeFromSuperview() - - def area(self): - """Area of our view that is occupied by the viewport.""" - return NSRect(self.view.bounds().origin, self.placement.size) - - def get_width(self): - return self.view.frame().size.width - - def get_height(self): - return self.view.frame().size.height - - def queue_redraw(self): - opaque_view = self.view.opaqueAncestor() - if opaque_view is not None: - rect = opaque_view.convertRect_fromView_(self.area(), self.view) - opaque_view.setNeedsDisplayInRect_(rect) - - def redraw_now(self): - self.view.displayRect_(self.area()) - -class BorrowedViewport(Viewport): - """Used when a widget uses the NSView of one of it's ancestors. We store - the view that we borrow as well as an NSRect specifying where on that view - we are placed. - """ - def __init__(self, view, placement): - self.view = view - self.placement = placement - - def at_position(self, rect): - return self.placement == rect - - def reposition(self, rect): - self.placement = rect - - def remove(self): - pass - - def area(self): - return self.placement - - def get_width(self): - return self.placement.size.width - - def get_height(self): - return self.placement.size.height diff --git a/mvc/widgets/osx/widgetset.py b/mvc/widgets/osx/widgetset.py deleted file mode 100644 index 1203566..0000000 --- a/mvc/widgets/osx/widgetset.py +++ /dev/null @@ -1,58 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -""".widgetset -- Contains all the -platform-specific widgets. This module doesn't have any actual code in it, it -just imports the widgets from their actual locations. -""" - -from .const import * -from .control import (TextEntry, NumberEntry, - SecureTextEntry, MultilineTextEntry) -from .control import Checkbox, Button, OptionMenu, RadioButtonGroup, RadioButton -from .customcontrol import (CustomButton, - ContinuousCustomButton, CustomSlider, DragableCustomButton) -from .contextmenu import ContextMenu -from .drawing import DrawingContext, ImageSurface, Gradient -from .drawingwidgets import DrawingArea, Background -from .rect import Rect -from .layout import VBox, HBox, Alignment, Table, Scroller, Expander, TabContainer, DetachedWindowHolder -from .window import Window, MainWindow, Dialog, FileSaveDialog, FileOpenDialog -from .window import DirectorySelectDialog, AboutDialog, AlertDialog, PreferencesWindow, DonateWindow, DialogWindow, get_first_time_dialog_coordinates -from .simple import (Image, ImageDisplay, Label, - SolidBackground, ClickableImageButton, AnimatedImageDisplay, - ProgressBar, HLine) -from .tableview import (TableView, TableColumn, - CellRenderer, CustomCellRenderer, ImageCellRenderer, - CheckboxCellRenderer, - CUSTOM_HEADER_HEIGHT) -from .tablemodel import (TableModel, - TreeTableModel) -from .osxmenus import (MenuBar, Menu, Separator, MenuItem, RadioMenuItem, CheckMenuItem) -from .base import Widget diff --git a/mvc/widgets/osx/widgetupdates.py b/mvc/widgets/osx/widgetupdates.py deleted file mode 100644 index 30677c2..0000000 --- a/mvc/widgets/osx/widgetupdates.py +++ /dev/null @@ -1,72 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -"""widgetupdates.py -- Handle updates to our widgets -""" - -from PyObjCTools import AppHelper - -class SizeRequestManager(object): - """Helper object to manage size requests - - If something changes in a widget that makes us want to request a new size, - we avoid calculating it immediately. The reason is that the - new-size-request will cascade all the way up the widget tree, and then - result in our widget being placed. We don't necessary want all of this - action to happen while we are in the middle of handling an event - (especially with TableView). It's also inefficient to calculate things - immediately, since we might do something else to invalidate the size - request in the current event. - - SizeRequestManager stores which widgets need to have their size - recalculated, then calls do_invalidate_size_request() using callAfter - """ - - def __init__(self): - self.widgets_to_request = set() - #app.widgetapp.connect("event-processed", self._on_event_processed) - - def add_widget(self, widget): - if len(self.widgets_to_request) == 0: - AppHelper.callAfter(self._run_requests) - self.widgets_to_request.add(widget) - - def _run_requests(self): - this_run = self.widgets_to_request - self.widgets_to_request = set() - for widget in this_run: - widget.do_invalidate_size_request() - - def _on_event_processed(self, app): - # once we finishing handling an event, process our size requests ASAP - # to avoid any potential weirdness. Note: that we also schedule a - # call using callAfter(), often that will do nothing, but it's - # possible size requests get scheduled outside of an event - while self.widgets_to_request: - self._run_requests() diff --git a/mvc/widgets/osx/window.py b/mvc/widgets/osx/window.py deleted file mode 100644 index b959333..0000000 --- a/mvc/widgets/osx/window.py +++ /dev/null @@ -1,896 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -""".window -- Top-level Window class. """ - -import logging - -from AppKit import * -from Foundation import * -from objc import YES, NO, nil -from PyObjCTools import AppHelper - -from mvc import signals -from mvc.widgets import widgetconst -import wrappermap -import osxmenus -from .helpers import NotificationForwarder -from .base import Widget, FlippedView -from .layout import VBox, HBox, Alignment -from .control import Button -from .simple import Label -from .rect import Rect, NSRectWrapper -from .utils import filename_to_unicode - -# Tracks all windows that haven't been destroyed. This makes sure there -# object stay alive as long as the window is alive. -alive_windows = set() - -class MiroResponderInterceptor(NSResponder): - """Intercepts cocoa events and gives our wrappers and chance to handle - them first. - """ - - def initWithResponder_(self, responder): - """Initialize a MiroResponderInterceptor - - We will give the wrapper for responder a chance to handle the event, - then pass it along to responder. - """ - self = super(MiroResponderInterceptor, self).init() - self.responder = responder - return self - - def keyDown_(self, event): - if self.sendKeyDownToWrapper_(event): - return # signal handler returned True, stop processing - - # If our responder is the last in the chain, we can stop intercepting - if self.responder.nextResponder() is None: - self.responder.keyDown_(event) - return - - # Here's the tricky part, we want to call keyDown_ on our responder, - # but if it doesn't handle the event, then it will pass it along to - # it's next responder. We need to set things up so that we will - # intercept that call. - - # Make a new MiroResponderInterceptor whose responder is the next - # responder down the chain. - next_intercepter = MiroResponderInterceptor.alloc().initWithResponder_( - self.responder.nextResponder()) - # Install the interceptor - self.responder.setNextResponder_(next_intercepter) - # Send event along - self.responder.keyDown_(event) - # Restore old nextResponder value - self.responder.setNextResponder_(next_intercepter.responder) - - def sendKeyDownToWrapper_(self, event): - """Give a keyDown event to the wrapper for our responder - - Return True if the wrapper handled the event - """ - key = event.charactersIgnoringModifiers() - if len(key) != 1 or not key.isalnum(): - key = osxmenus.REVERSE_KEYS_MAP.get(key) - mods = osxmenus.translate_event_modifiers(event) - wrapper = wrappermap.wrapper(self.responder) - if isinstance(wrapper, Widget) or isinstance(wrapper, Window): - if wrapper.emit('key-press', key, mods): - return True - return False - -class MiroWindow(NSWindow): - def initWithContentRect_styleMask_backing_defer_(self, rect, mask, - backing, defer): - self = NSWindow.initWithContentRect_styleMask_backing_defer_(self, - rect, mask, backing, defer) - self._last_focus_chain = None - return self - - def handleKeyDown_(self, event): - if self.handle_tab_navigation(event): - return - interceptor = MiroResponderInterceptor.alloc().initWithResponder_( - self.firstResponder()) - interceptor.keyDown_(event) - - def handle_tab_navigation(self, event): - """Handle tab navigation through the window. - - :returns: True if we handled the event - """ - keystr = event.charactersIgnoringModifiers() - if keystr[0] == NSTabCharacter: - # handle cycling through views with Tab. - self.focusNextKeyView_(True) - return True - elif keystr[0] == NSBackTabCharacter: - self.focusNextKeyView_(False) - return True - return False - - def acceptsMouseMovedEvents(self): - # HACK: for some reason calling setAcceptsMouseMovedEvents_() doesn't - # work, we have to forcefully override this method. - return NO - - def sendEvent_(self, event): - if event.type() == NSKeyDown: - self.handleKeyDown_(event) - else: - NSWindow.sendEvent_(self, event) - - def _calc_current_focus_wrapper(self): - responder = self.firstResponder() - while responder: - wrapper = wrappermap.wrapper(responder) - # check if we have a wrapper for the view, if not try the parent - # view - if wrapper is not None: - return wrapper - responder = responder.superview() - return None - - def focusNextKeyView_(self, is_forward): - current_focus = self._calc_current_focus_wrapper() - my_wrapper = wrappermap.wrapper(self) - next_focus = my_wrapper.get_next_tab_focus(current_focus, is_forward) - if next_focus is not None: - next_focus.focus() - - def draggingEntered_(self, info): - wrapper = wrappermap.wrapper(self) - return wrapper.draggingEntered_(info) or NSDragOperationNone - - def draggingUpdated_(self, info): - wrapper = wrappermap.wrapper(self) - return wrapper.draggingUpdated_(info) or NSDragOperationNone - - def draggingExited_(self, info): - wrapper = wrappermap.wrapper(self) - wrapper.draggingExited_(info) - - def prepareForDragOperation_(self, info): - wrapper = wrappermap.wrapper(self) - return wrapper.prepareForDragOperation_(info) or NO - - def performDragOperation_(self, info): - wrapper = wrappermap.wrapper(self) - return wrapper.performDragOperation_(info) or NO - -class MainMiroWindow(MiroWindow): - def isMovableByWindowBackground(self): - return YES - -class Window(signals.SignalEmitter): - """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class.""" - def __init__(self, title, rect=None): - signals.SignalEmitter.__init__(self) - self.create_signal('active-change') - self.create_signal('will-close') - self.create_signal('did-move') - self.create_signal('key-press') - self.create_signal('show') - self.create_signal('hide') - self.create_signal('on-shown') - self.create_signal('file-drag-motion') - self.create_signal('file-drag-received') - self.create_signal('file-drag-leave') - self.is_closing = False - if rect is None: - rect = Rect(0, 0, 470, 600) - self.nswindow = MainMiroWindow.alloc().initWithContentRect_styleMask_backing_defer_( - rect.nsrect, - self.get_style_mask(), - NSBackingStoreBuffered, - NO) - self.nswindow.setTitle_(title) - self.nswindow.setMinSize_(NSSize(470, 600)) - self.nswindow.setReleasedWhenClosed_(NO) - self.content_view = FlippedView.alloc().initWithFrame_(rect.nsrect) - self.content_view.setAutoresizesSubviews_(NO) - self.nswindow.setContentView_(self.content_view) - self.content_widget = None - self.view_notifications = NotificationForwarder.create(self.content_view) - self.view_notifications.connect(self.on_frame_change, 'NSViewFrameDidChangeNotification') - self.window_notifications = NotificationForwarder.create(self.nswindow) - self.window_notifications.connect(self.on_activate, 'NSWindowDidBecomeMainNotification') - self.window_notifications.connect(self.on_deactivate, 'NSWindowDidResignMainNotification') - self.window_notifications.connect(self.on_did_move, 'NSWindowDidMoveNotification') - self.window_notifications.connect(self.on_will_close, 'NSWindowWillCloseNotification') - wrappermap.add(self.nswindow, self) - alive_windows.add(self) - - def get_next_tab_focus(self, current, is_forward): - """Return the next widget to cycle through for keyboard focus - - Subclasses can override this to for find-grained control of keyboard - focus. - - :param current: currently-focused widget - :param is_forward: are we tabbing forward? - """ - return None - - # XXX Use MainWindow not Window for MVCStyle/MiroStyle - def get_style_mask(self): - return (NSTitledWindowMask | NSClosableWindowMask | - NSMiniaturizableWindowMask) - - def set_title(self, title): - self.nswindow.setTitle_(title) - - def get_title(self): - return self.nswindow.title() - - def on_frame_change(self, notification): - self.place_child() - - def on_activate(self, notification): - self.emit('active-change') - - def on_deactivate(self, notification): - self.emit('active-change') - - def on_did_move(self, notification): - self.emit('did-move') - - def on_will_close(self, notification): - # unset the first responder. This allows text entry widgets to get - # the NSControlTextDidEndEditingNotification - if self.is_closing: - logging.info('on_will_close: already closing') - return - self.is_closing = True - self.nswindow.makeFirstResponder_(nil) - self.emit('will-close') - self.emit('hide') - self.is_closing = False - - def is_active(self): - return self.nswindow.isMainWindow() - - def is_visible(self): - return self.nswindow.isVisible() - - def show(self): - if self not in alive_windows: - raise ValueError("Window destroyed") - self.nswindow.makeKeyAndOrderFront_(nil) - self.nswindow.makeMainWindow() - self.emit('show') - # Cocoa doesn't apply default selections as forcefully as GTK, so - # currently there's no need for on-shown to actually wait until the - # window has been shown here - self.emit('on-shown') - - def close(self): - self.nswindow.close() - - def destroy(self): - self.close() - self.window_notifications.disconnect() - self.view_notifications.disconnect() - self.nswindow.setContentView_(nil) - wrappermap.remove(self.nswindow) - alive_windows.discard(self) - self.nswindow = None - - def place_child(self): - rect = self.nswindow.contentRectForFrameRect_(self.nswindow.frame()) - self.content_widget.place(NSRect(NSPoint(0, 0), rect.size), - self.content_view) - - def hookup_content_widget_signals(self): - self.size_req_handler = self.content_widget.connect('size-request-changed', - self.on_content_widget_size_request_change) - - def unhook_content_widget_signals(self): - self.content_widget.disconnect(self.size_req_handler) - self.size_req_handler = None - - def on_content_widget_size_request_change(self, widget, old_size): - self.update_size_constraints() - - def set_content_widget(self, widget): - if self.content_widget: - self.content_widget.remove_viewport() - self.unhook_content_widget_signals() - self.content_widget = widget - self.hookup_content_widget_signals() - self.place_child() - self.update_size_constraints() - - def update_size_constraints(self): - width, height = self.content_widget.get_size_request() - # It is possible the window is torn down between the size invalidate - # request and the actual size invalidation invocation. So check - # to see if nswindow is there if not then do not do anything. - if self.nswindow: - # FIXME: I'm not sure that this code does what we want it to do. - # It enforces the min-size when the user drags the window, but I - # think it should also call setContentSize_ if the window is - # currently too small to fit the content - BDK - self.nswindow.setContentMinSize_(NSSize(width, height)) - rect = self.nswindow.contentRectForFrameRect_(self.nswindow.frame()) - if rect.size.width < width or rect.size.height < height: - logging.warn("Content widget too large for this window " - "size available: %dx%d widget size: %dx%d", - rect.size.width, rect.size.height, width, height) - - def get_content_widget(self): - return self.content_widget - - def get_frame(self): - frame = self.nswindow.frame() - frame.size.height -= 22 - return NSRectWrapper(frame) - - def connect_menu_keyboard_shortcuts(self): - # All OS X windows are connected to the menu shortcuts - pass - - def accept_file_drag(self, val): - if not val: - self.drag_dest = None - else: - self.drag_dest = NSDragOperationCopy - self.nswindow.registerForDraggedTypes_([NSFilenamesPboardType]) - - def prepareForDragOperation_(self, info): - return NO if self.drag_dest is None else YES - - def performDragOperation_(self, info): - pb = info.draggingPasteboard() - available_types = set(pb.types()) & set([NSFilenamesPboardType]) - drag_ok = False - if available_types: - type_ = available_types.pop() - # DANCE! Everybody dance for portable Python code! - values = [unicode( - NSURL.fileURLWithPath_(v).filePathURL()).encode('utf-8') - for v in list(pb.propertyListForType_(type_))] - self.emit('file-drag-received', values) - drag_ok = True - self.draggingExited_(info) - return drag_ok - - def draggingEntered_(self, info): - return self.draggingUpdated_(info) - - def draggingUpdated_(self, info): - self.emit('file-drag-motion') - return self.drag_dest - - def draggingExited_(self, info): - self.emit('file-drag-leave') - - def center(self): - self.nswindow.center() - -class MainWindow(Window): - def __init__(self, title, rect): - Window.__init__(self, title, rect) - self.nswindow.setReleasedWhenClosed_(NO) - - def close(self): - self.nswindow.orderOut_(nil) - -class DialogBase(object): - def __init__(self): - self.sheet_parent = None - def set_transient_for(self, window): - self.sheet_parent = window - -class MiroPanel(NSPanel): - def cancelOperation_(self, event): - wrappermap.wrapper(self).end_with_code(-1) - -class Dialog(DialogBase): - def __init__(self, title, description=None): - DialogBase.__init__(self) - self.title = title - self.description = description - self.buttons = [] - self.extra_widget = None - self.window = None - self.running = False - - def add_button(self, text): - button = Button(text) - button.set_size(widgetconst.SIZE_NORMAL) - button.connect('clicked', self.on_button_clicked, len(self.buttons)) - self.buttons.append(button) - - def on_button_clicked(self, button, code): - self.end_with_code(code) - - def end_with_code(self, code): - if self.sheet_parent is not None: - NSApp().endSheet_returnCode_(self.window, code) - else: - NSApp().stopModalWithCode_(code) - - def build_text(self): - vbox = VBox(spacing=6) - if self.description is not None: - description_label = Label(self.description, wrap=True) - description_label.set_bold(True) - description_label.set_size_request(360, -1) - vbox.pack_start(description_label) - return vbox - - def build_buttons(self): - hbox = HBox(spacing=12) - for button in reversed(self.buttons): - hbox.pack_start(button) - alignment = Alignment(xalign=1.0, yscale=1.0) - alignment.add(hbox) - return alignment - - def build_content(self): - vbox = VBox(spacing=12) - vbox.pack_start(self.build_text()) - if self.extra_widget: - vbox.pack_start(self.extra_widget) - vbox.pack_start(self.build_buttons()) - alignment = Alignment(xscale=1.0, yscale=1.0) - alignment.set_padding(12, 12, 17, 17) - alignment.add(vbox) - return alignment - - def build_window(self): - self.content_widget = self.build_content() - width, height = self.content_widget.get_size_request() - width = max(width, 400) - window = MiroPanel.alloc() - window.initWithContentRect_styleMask_backing_defer_( - NSMakeRect(400, 400, width, height), - NSTitledWindowMask, NSBackingStoreBuffered, NO) - view = FlippedView.alloc().initWithFrame_(NSMakeRect(0, 0, width, - height)) - window.setContentView_(view) - window.setTitle_(self.title) - self.content_widget.place(view.frame(), view) - if self.buttons: - self.buttons[0].make_default() - return window - - def hookup_content_widget_signals(self): - self.size_req_handler = self.content_widget.connect( - 'size-request-changed', - self.on_content_widget_size_request_change) - - def unhook_content_widget_signals(self): - self.content_widget.disconnect(self.size_req_handler) - self.size_req_handler = None - - def on_content_widget_size_request_change(self, widget, old_size): - width, height = self.content_widget.get_size_request() - # It is possible the window is torn down between the size invalidate - # request and the actual size invalidation invocation. So check - # to see if nswindow is there if not then do not do anything. - if self.window and (width, height) != old_size: - self.change_content_size(width, height) - - def change_content_size(self, width, height): - content_rect = self.window.contentRectForFrameRect_( - self.window.frame()) - # Cocoa's coordinate system is funky, adjust y so that the top stays - # in place - content_rect.origin.y += (content_rect.size.height - height) - # change our frame to fit the new content. It would be nice to - # animate the change, but timers don't work when we are displaying a - # modal dialog - content_rect.size = NSSize(width, height) - new_frame = self.window.frameRectForContentRect_(content_rect) - self.window.setFrame_display_(new_frame, NO) - # Need to call place() again, since our window has changed size - contentView = self.window.contentView() - self.content_widget.place(contentView.frame(), contentView) - - def run(self): - self.window = self.build_window() - wrappermap.add(self.window, self) - self.hookup_content_widget_signals() - self.running = True - if self.sheet_parent is None: - response = NSApp().runModalForWindow_(self.window) - if self.window: - self.window.close() - else: - delegate = SheetDelegate.alloc().init() - NSApp().beginSheet_modalForWindow_modalDelegate_didEndSelector_contextInfo_( - self.window, self.sheet_parent.nswindow, - delegate, 'sheetDidEnd:returnCode:contextInfo:', 0) - response = NSApp().runModalForWindow_(self.window) - if self.window: - # self.window won't be around if we call destroy() to cancel - # the dialog - self.window.orderOut_(nil) - self.running = False - self.unhook_content_widget_signals() - - if response < 0: - return -1 - return response - - def destroy(self): - if self.running: - NSApp().stopModalWithCode_(-1) - - if self.window is not None: - self.window.setContentView_(None) - self.window.close() - self.window = None - self.buttons = None - self.extra_widget = None - - def set_extra_widget(self, widget): - self.extra_widget = widget - - def get_extra_widget(self): - return self.extra_widget - -class SheetDelegate(NSObject): - @AppHelper.endSheetMethod - def sheetDidEnd_returnCode_contextInfo_(self, sheet, return_code, info): - NSApp().stopModalWithCode_(return_code) - -class FileDialogBase(DialogBase): - def __init__(self): - DialogBase.__init__(self) - self._types = None - self._filename = None - self._directory = None - self._filter_on_run = True - - def run(self): - self._panel.setAllowedFileTypes_(self._types) - if self.sheet_parent is None: - if self._filter_on_run: - response = self._panel.runModalForDirectory_file_types_(self._directory, self._filename, self._types) - else: - response = self._panel.runModalForDirectory_file_(self._directory, self._filename) - else: - delegate = SheetDelegate.alloc().init() - if self._filter_on_run: - self._panel.beginSheetForDirectory_file_types_modalForWindow_modalDelegate_didEndSelector_contextInfo_( - self._directory, self._filename, self._types, - self.sheet_parent.nswindow, delegate, 'sheetDidEnd:returnCode:contextInfo:', 0) - else: - self._panel.beginSheetForDirectory_file_modalForWindow_modalDelegate_didEndSelector_contextInfo_( - self._directory, self._filename, - self.sheet_parent.nswindow, delegate, 'sheetDidEnd:returnCode:contextInfo:', 0) - response = NSApp().runModalForWindow_(self._panel) - self._panel.orderOut_(nil) - return response - -class FileSaveDialog(FileDialogBase): - def __init__(self, title): - FileDialogBase.__init__(self) - self._title = title - self._panel = NSSavePanel.savePanel() - self._panel.setCanChooseFiles_(YES) - self._panel.setCanChooseDirectories_(NO) - self._filename = None - self._filter_on_run = False - - def set_filename(self, s): - self._filename = filename_to_unicode(s) - - def get_filename(self): - # Use encode('utf-8') instead of unicode_to_filename, because - # unicode_to_filename has code to make sure nextFilename works, but it's - # more important here to not change the filename. - return self._filename.encode('utf-8') - - def run(self): - response = FileDialogBase.run(self) - if response == NSFileHandlingPanelOKButton: - self._filename = self._panel.filename() - return 0 - self._filename = "" - - def destroy(self): - self._panel = None - - set_path = set_filename - get_path = get_filename - -class FileOpenDialog(FileDialogBase): - def __init__(self, title): - FileDialogBase.__init__(self) - self._title = title - self._panel = NSOpenPanel.openPanel() - self._panel.setCanChooseFiles_(YES) - self._panel.setCanChooseDirectories_(NO) - self._filenames = None - - def set_select_multiple(self, value): - if value: - self._panel.setAllowsMultipleSelection_(YES) - else: - self._panel.setAllowsMultipleSelection_(NO) - - def set_directory(self, d): - self._directory = filename_to_unicode(d) - - def set_filename(self, s): - self._filename = filename_to_unicode(s) - - def add_filters(self, filters): - self._types = [] - for _, t in filters: - self._types += t - - def get_filename(self): - if self._filenames is None: - # canceled - return None - return self.get_filenames()[0] - - def get_filenames(self): - if self._filenames is None: - # canceled - return [] - # Use encode('utf-8') instead of unicode_to_filename, because - # unicode_to_filename has code to make sure nextFilename works, but it's - # more important here to not change the filename. - return [f.encode('utf-8') for f in self._filenames] - - def run(self): - response = FileDialogBase.run(self) - if response == NSFileHandlingPanelOKButton: - self._filenames = self._panel.filenames() - return 0 - self._filename = '' - self._filenames = None - - def destroy(self): - self._panel = None - - set_path = set_filename - get_path = get_filename - -class DirectorySelectDialog(FileDialogBase): - def __init__(self, title): - FileDialogBase.__init__(self) - self._title = title - self._panel = NSOpenPanel.openPanel() - self._panel.setCanChooseFiles_(NO) - self._panel.setCanChooseDirectories_(YES) - self._directory = None - - def set_directory(self, d): - self._directory = filename_to_unicode(d) - - def get_directory(self): - # Use encode('utf-8') instead of unicode_to_filename, because - # unicode_to_filename has code to make sure nextFilename - # works, but it's more important here to not change the - # filename. - return self._directory.encode('utf-8') - - def run(self): - response = FileDialogBase.run(self) - if response == NSFileHandlingPanelOKButton: - self._directory = self._panel.filenames()[0] - return 0 - self._directory = "" - - def destroy(self): - self._panel = None - - set_path = set_directory - get_path = get_directory - -class AboutDialog(DialogBase): - def run(self): - optionsDictionary = dict() - #revision = app.config.get(prefs.APP_REVISION_NUM) - #if revision: - # optionsDictionary['Version'] = revision - if not optionsDictionary: - optionsDictionary = nil - NSApplication.sharedApplication().orderFrontStandardAboutPanelWithOptions_(optionsDictionary) - def destroy(self): - pass - -class AlertDialog(DialogBase): - def __init__(self, title, message, alert_type): - DialogBase.__init__(self) - self._nsalert = NSAlert.alloc().init(); - self._nsalert.setMessageText_(title) - self._nsalert.setInformativeText_(message) - self._nsalert.setAlertStyle_(alert_type) - def add_button(self, text): - self._nsalert.addButtonWithTitle_(text) - def run(self): - self._nsalert.runModal() - def destroy(self): - self._nsalert = nil - -class PreferenceItem(NSToolbarItem): - - def setPanel_(self, panel): - self.panel = panel - -class PreferenceToolbarDelegate(NSObject): - - def initWithPanels_identifiers_window_(self, panels, identifiers, window): - self = super(PreferenceToolbarDelegate, self).init() - self.panels = panels - self.identifiers = identifiers - self.window = window - return self - - def toolbarAllowedItemIdentifiers_(self, toolbar): - return self.identifiers - - def toolbarDefaultItemIdentifiers_(self, toolbar): - return self.identifiers - - def toolbarSelectableItemIdentifiers_(self, toolbar): - return self.identifiers - - def toolbar_itemForItemIdentifier_willBeInsertedIntoToolbar_(self, toolbar, - itemIdentifier, - flag): - panel = self.panels[itemIdentifier] - item = PreferenceItem.alloc().initWithItemIdentifier_(itemIdentifier) - item.setLabel_(unicode(panel[1])) - item.setImage_(NSImage.imageNamed_(u"pref_tab_%s" % itemIdentifier)) - item.setAction_("switchPreferenceView:") - item.setTarget_(self) - item.setPanel_(panel[0]) - return item - - def validateToolbarItem_(self, item): - return YES - - def switchPreferenceView_(self, sender): - self.window.do_select_panel(sender.panel, YES) - -class DialogWindow(Window): - def __init__(self, title, rect, allow_miniaturize=False): - self.allow_miniaturize = allow_miniaturize - Window.__init__(self, title, rect) - self.nswindow.setShowsToolbarButton_(NO) - - def get_style_mask(self): - mask = (NSTitledWindowMask | NSClosableWindowMask) - if self.allow_miniaturize: - mask |= NSMiniaturizableWindowMask - return mask - -class DonateWindow(Window): - def __init__(self, title): - Window.__init__(self, title, Rect(0, 0, 640, 440)) - self.panels = dict() - self.identifiers = list() - self.first_show = True - self.nswindow.setShowsToolbarButton_(NO) - self.nswindow.setReleasedWhenClosed_(NO) - self.app_notifications = NotificationForwarder.create(NSApp()) - self.app_notifications.connect(self.on_app_quit, - 'NSApplicationWillTerminateNotification') - - def destroy(self): - super(PreferencesWindow, self).destroy() - self.app_notifications.disconnect() - - def get_style_mask(self): - return (NSTitledWindowMask | NSClosableWindowMask | - NSMiniaturizableWindowMask) - - def show(self): - if self.first_show: - self.nswindow.center() - self.first_show = False - Window.show(self) - - def on_app_quit(self, notification): - self.close() - -class PreferencesWindow(Window): - def __init__(self, title): - Window.__init__(self, title, Rect(0, 0, 640, 440)) - self.panels = dict() - self.identifiers = list() - self.first_show = True - self.nswindow.setShowsToolbarButton_(NO) - self.nswindow.setReleasedWhenClosed_(NO) - self.app_notifications = NotificationForwarder.create(NSApp()) - self.app_notifications.connect(self.on_app_quit, - 'NSApplicationWillTerminateNotification') - - def destroy(self): - super(PreferencesWindow, self).destroy() - self.app_notifications.disconnect() - - def get_style_mask(self): - return (NSTitledWindowMask | NSClosableWindowMask | - NSMiniaturizableWindowMask) - - def append_panel(self, name, panel, title, image_name): - self.panels[name] = (panel, title) - self.identifiers.append(name) - - def finish_panels(self): - self.tbdelegate = PreferenceToolbarDelegate.alloc().initWithPanels_identifiers_window_(self.panels, self.identifiers, self) - toolbar = NSToolbar.alloc().initWithIdentifier_(u"Preferences") - toolbar.setAllowsUserCustomization_(NO) - toolbar.setDelegate_(self.tbdelegate) - - self.nswindow.setToolbar_(toolbar) - - def select_panel(self, index): - panel = self.identifiers[index] - self.nswindow.toolbar().setSelectedItemIdentifier_(panel) - self.do_select_panel(self.panels[panel][0], NO) - - def do_select_panel(self, panel, animate): - wframe = self.nswindow.frame() - vsize = list(panel.get_size_request()) - if vsize[0] < 650: - vsize[0] = 650 - if vsize[1] < 200: - vsize[1] = 200 - - toolbarHeight = wframe.size.height - self.nswindow.contentView().frame().size.height - wframe.origin.y += wframe.size.height - vsize[1] - toolbarHeight - wframe.size = (vsize[0], vsize[1] + toolbarHeight) - - self.set_content_widget(panel) - self.nswindow.setFrame_display_animate_(wframe, YES, animate) - - def show(self): - if self.first_show: - self.nswindow.center() - self.first_show = False - Window.show(self) - - def on_app_quit(self, notification): - self.close() - -def get_first_time_dialog_coordinates(width, height): - """Returns the coordinates for the first time dialog. - """ - # windowFrame is None on first run. in that case, we want - # to put librevideoconverter in the middle. - mainscreen = NSScreen.mainScreen() - rect = mainscreen.frame() - - x = (rect.size.width - width) / 2 - y = (rect.size.height - height) / 2 - - return x, y diff --git a/mvc/widgets/osx/wrappermap.py b/mvc/widgets/osx/wrappermap.py deleted file mode 100644 index 624a496..0000000 --- a/mvc/widgets/osx/wrappermap.py +++ /dev/null @@ -1,48 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -""".wrappermap -- Map NSViews and NSWindows to the -Widget that wraps them. -""" - -# Maps NSViews/NSWinows -> wrapper objects. -wrapper_mapping = dict() - -def wrapper(wrapped): - """Find the wrapper object for an NSView/NSWindow.""" - try: - return wrapper_mapping[wrapped] - except KeyError: - return None - -def add(wrapped, wrapper): - wrapper_mapping[wrapped] = wrapper - -def remove(wrapped): - del wrapper_mapping[wrapped] diff --git a/mvc/widgets/tablescroll.py b/mvc/widgets/tablescroll.py deleted file mode 100644 index 841e62c..0000000 --- a/mvc/widgets/tablescroll.py +++ /dev/null @@ -1,154 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -"""tablescroll.py -- High-level scroll management. This ensures that behavior -like scroll_to_item works the same way across platforms. -""" - -from mvc.errors import WidgetActionError - - -class ScrollbarOwnerMixin(object): - """Scrollbar management for TableView. - - External methods have undecorated names; internal methods start with _. - - External methods: - - handle failure themselves (e.g. return None or retry later) - - return basic data types (e.g. (x, y) tuples) - - use "tree" coordinates - - Internal methods (intended to be used by ScrollbarOwnerMixin and the - platform implementations): - - raise WidgetActionError subclasses on failure - - use Rect/Point structs - - also use "tree" coordinates - """ - def __init__(self, _work_around_17153=False): - self.__work_around_17153 = _work_around_17153 - self._scroll_to_iter_callback = None - self.create_signal('scroll-range-changed') - - def scroll_to_iter(self, iter_, manual=True, recenter=False): - """Scroll the given item into view. - - manual: scroll even if we were not following the playing item - recenter: scroll even if item is in top half of view - """ - try: - item = self._get_item_area(iter_) - visible = self._get_visible_area() - manually_scrolled = self._manually_scrolled - except WidgetActionError: - if self._scroll_to_iter_callback: - # We just retried and failed. Do nothing; we will retry again - # next time scrollable range changes. - return - # We just tried and failed; schedule a retry when the scrollable - # range changes. - self._scroll_to_iter_callback = self.connect('scroll-range-changed', - lambda *a: self.scroll_to_iter(iter_, manual, recenter)) - return - # If the above succeeded, we know the iter's position; this means we can - # set_scroll_position to that position. That may work now or be - # postponed until later, but either way we're done with scroll_to_iter. - if self._scroll_to_iter_callback: - self.disconnect(self._scroll_to_iter_callback) - self._scroll_to_iter_callback = None - visible_bottom = visible.y + visible.height - visible_middle = visible.y + visible.height // 2 - item_bottom = item.y + item.height - item_middle = item.y + item.height // 2 - in_top = item_bottom >= visible.y and item.y <= visible_middle - in_bottom = item_bottom >= visible_middle and item.y <= visible_bottom - if self._should_scroll( - manual, in_top, in_bottom, recenter, manually_scrolled): - destination = item_middle - visible.height // 2 - self._set_vertical_scroll(destination) - # set_scroll_position will take care of scroll to the position when - # possible; this may or may not be now, but our work here is done. - - def set_scroll_position(self, position, restore_only=False, - _hack_for_17153=False): - """Scroll the top left corner to the given (x, y) offset from the origin - of the view. - - restore_only: set the value only if no other value has been set yet - """ - if _hack_for_17153 and not self.__work_around_17153: - return - if not restore_only or not self._position_set: - self._set_scroll_position(position) - - @classmethod - def _should_scroll(cls, - manual, in_top, in_bottom, recenter, manually_scrolled): - if not manual and manually_scrolled: - # The user has moved the scrollbars since we last autoscrolled, and - # we're deciding whether we should resume autoscrolling. - # We want to do that when the currently-playing item catches up to - # the center of the screen i.e. is part above the center, part below - return in_top and in_bottom - # This is a manual scroll, or we're already autoscrolling - so we no - # longer need to worry about either manual or manually_scrolled - if in_top: - # The item is in the top half; let playback catch up with the - # current scroll position, unless recentering has been requested - return recenter - if in_bottom: - # We land here when: - # - playback has begun with an item in the bottom half of the screen - # - scroll is following sequential playback - # Either way we want to jump down to the item. - return True - # We're scrolling to an item that's not in view because: - # - playback has begun with an item that is out of sight - # - we're autoscrolling on shuffle - # Either way we want to show the item. - return True - - def reset_scroll(self): - """To scroll back to the origin; platform code might want to do - something special to forget the current position when this happens. - """ - self.set_scroll_position((0, 0)) - - def get_scroll_position(self): - """Returns the current scroll position, or None if not ready.""" - try: - return tuple(self._get_scroll_position()) - except WidgetActionError: - return None - - def _set_vertical_scroll(self, pos): - """Helper to set our vertical position without affecting our horizontal - position. - """ - # FIXME: shouldn't reset horizontal position - self.set_scroll_position((0, pos)) diff --git a/mvc/widgets/tableselection.py b/mvc/widgets/tableselection.py deleted file mode 100644 index d087d34..0000000 --- a/mvc/widgets/tableselection.py +++ /dev/null @@ -1,220 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -"""tableselection.py -- High-level selection management. Subclasses defined in -the platform tableview modules provide the platform-specific methods used here. -""" - -from contextlib import contextmanager - -from mvc.errors import WidgetActionError, WidgetUsageError - -class SelectionOwnerMixin(object): - """Encapsulates the selection functionality of a TableView, for - consistent behavior across platforms. - - Emits: - :signal selection-changed: the selection has been changed - :signal selection-invalid: the item selected can no longer be selected - :signal deselected: all items have been deselected - """ - def __init__(self): - self._ignore_selection_changed = 0 - self._allow_multiple_select = None - self.create_signal('selection-changed') - self.create_signal('selection-invalid') - self.create_signal('deselected') - - @property - def allow_multiple_select(self): - """Return whether the widget allows multiple selection.""" - if self._allow_multiple_select is None: - self._allow_multiple_select = self._get_allow_multiple_select() - return self._allow_multiple_select - - @allow_multiple_select.setter - def allow_multiple_select(self, allow): - """Set whether to allow multiple selection; this method is expected - always to work. - """ - if self._allow_multiple_select != allow: - self._set_allow_multiple_select(allow) - self._allow_multiple_select = allow - - @property - def num_rows_selected(self): - """Override on platforms with a way to count rows without having to - retrieve them. - """ - if self.allow_multiple_select: - return len(self._get_selected_iters()) - else: - return int(self._get_selected_iter() is not None) - - def select(self, iter_, signal=False): - """Try to select an iter. - - :raises WidgetActionError: iter does not exist or is not selectable - """ - self.select_iters((iter_,), signal) - - def select_iters(self, iters, signal=False): - """Try to select multiple iters (signaling at most once). - - :raises WidgetActionError: iter does not exist or is not selectable - """ - with self._ignoring_changes(not signal): - for iter_ in iters: - self._select(iter_) - if not all(self._is_selected(iter_) for iter_ in iters): - raise WidgetActionError("the specified iter cannot be selected") - - def is_selected(self, iter_): - """Test if an iter is selected""" - return self._is_selected(iter_) - - def unselect(self, iter_): - """Unselect an Iter. Fails silently if the Iter is not selected. - """ - self._validate_iter(iter_) - with self._ignoring_changes(): - self._unselect(iter_) - - def unselect_iters(self, iters): - """Unselect iters. Fails silently if the iters are not selected.""" - with self._ignoring_changes(): - for iter_ in iters: - self.unselect(iter_) - - def unselect_all(self, signal=True): - """Unselect all. emits only the 'deselected' signal.""" - with self._ignoring_changes(): - self._unselect_all() - if signal: - self.emit('deselected') - - def on_selection_changed(self, _widget_or_notification): - """When we receive a selection-changed signal, we forward it if we're - not in a 'with _ignoring_changes' block. Selection-changed - handlers are run in an ignoring block, and anything that changes the - selection to reflect the current state. - """ - # don't bother sending out a second selection-changed signal if - # the handler changes the selection (#15767) - if not self._ignore_selection_changed: - with self._ignoring_changes(): - self.emit('selection-changed') - - def get_selection_as_strings(self): - """Returns the current selection as a list of strings. - """ - return [self._iter_to_string(iter_) for iter_ in self.get_selection()] - - def set_selection_as_strings(self, selected): - """Given a list of selection strings, selects each Iter represented by - the strings. - - Raises WidgetActionError upon failure. - """ - # iter may not be destringable (yet) - bounds error - # destringed iter not selectable if parent isn't open (yet) - self.set_selection(self._iter_from_string(sel) for sel in selected) - - def get_cursor(self): - """Get the location of the keyboard cursor for the tableview. - - Returns a string that represents the row that the keyboard cursor is - on. - """ - - def set_cursor(self, location): - """Set the location of the keyboard cursor for the tableview. - - :param location: return value from a call to get_cursor() - - Raises WidgetActionError upon failure. - """ - - def get_selection(self): - """Returns a list of GTK Iters. Works regardless of whether multiple - selection is enabled. - """ - return self._get_selected_iters() - - def get_selected(self): - """Return the single selected item. - - :raises WidgetUsageError: multiple selection is enabled - """ - if self.allow_multiple_select: - raise WidgetUsageError("table allows multiple selection") - return self._get_selected_iter() - - def _validate_iter(self, iter_): - """Check whether an iter is valid. - - :raises WidgetDomainError: the iter is not valid - :raises WidgetActionError: there is no model right now - """ - - @contextmanager - def _ignoring_changes(self, ignoring=True): - """Use this with with to prevent sending signals when we're changing - our own selection; that way, when we get a signal, we know it's - something important. - """ - if ignoring: - self._ignore_selection_changed += 1 - try: - yield - finally: - if ignoring: - self._ignore_selection_changed -= 1 - - @contextmanager - def preserving_selection(self): - """Prevent selection changes in a block from having any effect or - sticking - no signals will be sent, and the selection will be restored - to its original value when the block exits. - """ - iters = self._get_selected_iters() - with self._ignoring_changes(): - try: - yield - finally: - self.set_selection(iters) - - def set_selection(self, iters, signal=False): - """Set the selection to the given iters, replacing any previous - selection and signaling at most once. - """ - self.unselect_all(signal=False) - for iter_ in iters: - self.select(iter_, signal=False) - if signal: self.emit('selection-changed') diff --git a/mvc/widgets/widgetconst.py b/mvc/widgets/widgetconst.py deleted file mode 100644 index bbb513c..0000000 --- a/mvc/widgets/widgetconst.py +++ /dev/null @@ -1,44 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -"""``miro.frontends.widgets.widgetconst`` -- Constants for the widgets -frontend. -""" - -# Control sizes -SIZE_NORMAL = -1 -SIZE_SMALL = -2 - -TEXT_JUSTIFY_LEFT = 0 -TEXT_JUSTIFY_RIGHT = 1 -TEXT_JUSTIFY_CENTER = 2 - -# cursors -CURSOR_NORMAL = 0 -CURSOR_POINTING_HAND = 1 diff --git a/mvc/widgets/widgetutil.py b/mvc/widgets/widgetutil.py deleted file mode 100644 index 5fb3db2..0000000 --- a/mvc/widgets/widgetutil.py +++ /dev/null @@ -1,225 +0,0 @@ -from math import pi as PI -from mvc.widgets import widgetset -from mvc.resources import image_path - -def make_surface(image_name, height=None): - path = image_path(image_name + '.png') - image = widgetset.Image(path) - if height is not None: - image = image.resize(image.width, height) - return widgetset.ImageSurface(image) - -def font_scale_from_osx_points(points): - """Create a font scale so that it's points large on OS X. - - Assumptions (these should be true for OS X) - - the default font size is 13pt - - the DPI is 72ppi - """ - return points / 13.0 - -def css_to_color(css_string): - parts = (css_string[1:3], css_string[3:5], css_string[5:7]) - return tuple((int(value, 16) / 255.0) for value in parts) - -def align(widget, xalign=0, yalign=0, xscale=0, yscale=0, - top_pad=0, bottom_pad=0, left_pad=0, right_pad=0): - """Create an alignment, then add widget to it and return the alignment. - """ - alignment = widgetset.Alignment(xalign, yalign, xscale, yscale, - top_pad, bottom_pad, left_pad, right_pad) - alignment.add(widget) - return alignment - -def align_center(widget, top_pad=0, bottom_pad=0, left_pad=0, right_pad=0): - """Wrap a widget in an Alignment that will center it horizontally. - """ - return align(widget, 0.5, 0, 0, 1, - top_pad, bottom_pad, left_pad, right_pad) - -def align_right(widget, top_pad=0, bottom_pad=0, left_pad=0, right_pad=0): - """Wrap a widget in an Alignment that will align it left. - """ - return align(widget, 1, 0, 0, 1, top_pad, bottom_pad, left_pad, right_pad) - -def align_left(widget, top_pad=0, bottom_pad=0, left_pad=0, right_pad=0): - """Wrap a widget in an Alignment that will align it right. - """ - return align(widget, 0, 0, 0, 1, top_pad, bottom_pad, left_pad, right_pad) - -def align_middle(widget, top_pad=0, bottom_pad=0, left_pad=0, right_pad=0): - """Wrap a widget in an Alignment that will center it vertically. - """ - return align(widget, 0, 0.5, 1, 0, - top_pad, bottom_pad, left_pad, right_pad) - -def align_top(widget, top_pad=0, bottom_pad=0, left_pad=0, right_pad=0): - """Wrap a widget in an Alignment that will align to the top. - """ - return align(widget, 0, 0, 1, 0, top_pad, bottom_pad, left_pad, right_pad) - -def align_bottom(widget, top_pad=0, bottom_pad=0, left_pad=0, right_pad=0): - """Wrap a widget in an Alignment that will align to the bottom. - """ - return align(widget, 0, 1, 1, 0, top_pad, bottom_pad, left_pad, right_pad) - -def pad(widget, top=0, bottom=0, left=0, right=0): - """Wrap a widget in an Alignment that will pad it. - """ - alignment = widgetset.Alignment(0, 0, 1, 1, - top, bottom, left, right) - alignment.add(widget) - return alignment - -def round_rect(context, x, y, width, height, edge_radius): - """Specifies path of a rectangle with rounded corners. - """ - edge_radius = min(edge_radius, min(width, height)/2.0) - inner_width = width - edge_radius*2 - inner_height = height - edge_radius*2 - x_inner1 = x + edge_radius - x_inner2 = x + width - edge_radius - y_inner1 = y + edge_radius - y_inner2 = y + height - edge_radius - - context.move_to(x+edge_radius, y) - context.rel_line_to(inner_width, 0) - context.arc(x_inner2, y_inner1, edge_radius, -PI/2, 0) - context.rel_line_to(0, inner_height) - context.arc(x_inner2, y_inner2, edge_radius, 0, PI/2) - context.rel_line_to(-inner_width, 0) - context.arc(x_inner1, y_inner2, edge_radius, PI/2, PI) - context.rel_line_to(0, -inner_height) - context.arc(x_inner1, y_inner1, edge_radius, PI, PI*3/2) - -def round_rect_reverse(context, x, y, width, height, edge_radius): - """Specifies path of a rectangle with rounded corners. - - This specifies the rectangle in a counter-clockwise fashion. - """ - edge_radius = min(edge_radius, min(width, height)/2.0) - inner_width = width - edge_radius*2 - inner_height = height - edge_radius*2 - x_inner1 = x + edge_radius - x_inner2 = x + width - edge_radius - y_inner1 = y + edge_radius - y_inner2 = y + height - edge_radius - - context.move_to(x+edge_radius, y) - context.arc_negative(x_inner1, y_inner1, edge_radius, PI*3/2, PI) - context.rel_line_to(0, inner_height) - context.arc_negative(x_inner1, y_inner2, edge_radius, PI, PI/2) - context.rel_line_to(inner_width, 0) - context.arc_negative(x_inner2, y_inner2, edge_radius, PI/2, 0) - context.rel_line_to(0, -inner_height) - context.arc_negative(x_inner2, y_inner1, edge_radius, 0, -PI/2) - context.rel_line_to(-inner_width, 0) - -def circular_rect(context, x, y, width, height): - """Make a path for a rectangle with the left/right side being circles. - """ - radius = height / 2.0 - inner_width = width - height - inner_y = y + radius - inner_x1 = x + radius - inner_x2 = inner_x1 + inner_width - - context.move_to(inner_x1, y) - context.rel_line_to(inner_width, 0) - context.arc(inner_x2, inner_y, radius, -PI/2, PI/2) - context.rel_line_to(-inner_width, 0) - context.arc(inner_x1, inner_y, radius, PI/2, -PI/2) - -def circular_rect_negative(context, x, y, width, height): - """The same path as ``circular_rect()``, but going counter clockwise. - """ - radius = height / 2.0 - inner_width = width - height - inner_y = y + radius - inner_x1 = x + radius - inner_x2 = inner_x1 + inner_width - - context.move_to(inner_x1, y) - context.arc_negative(inner_x1, inner_y, radius, -PI/2, PI/2) - context.rel_line_to(inner_width, 0) - context.arc_negative(inner_x2, inner_y, radius, PI/2, -PI/2) - context.rel_line_to(-inner_width, 0) - -class Shadow(object): - """Encapsulates all parameters required to draw shadows. - """ - def __init__(self, color, opacity, offset, blur_radius): - self.color = color - self.opacity = opacity - self.offset = offset - self.blur_radius = blur_radius - -class ThreeImageSurface(object): - """Takes a left, center and right image and draws them to an arbitrary - width. If the width is greater than the combined width of the 3 images, - then the center image will be tiled to compensate. - - Example: - - >>> timelinebar = ThreeImageSurface("timelinebar") - - This creates a ``ThreeImageSurface`` using the images - ``images/timelinebar_left.png``, ``images/timelinebar_center.png``, and - ``images/timelinebar_right.png``. - - Example: - - >>> timelinebar = ThreeImageSurface() - >>> img_left = make_surface("timelinebar_left") - >>> img_center = make_surface("timelinebar_center") - >>> img_right = make_surface("timelinebar_right") - >>> timelinebar.set_images(img_left, img_center, img_right) - - This does the same thing, but allows you to explicitly set which images - get used. - """ - def __init__(self, basename=None, height=None): - self.left = self.center = self.right = None - self.height = 0 - self.width = None - if basename is not None: - left = make_surface(basename + '_left', height) - center = make_surface(basename + '_center', height) - right = make_surface(basename + '_right', height) - self.set_images(left, center, right) - - def set_images(self, left, center, right): - """Sets the left, center and right images to use. - """ - self.left = left - self.center = center - self.right = right - if not (self.left.height == self.center.height == self.right.height): - raise ValueError("Images aren't the same height") - self.height = self.left.height - - def set_width(self, width): - """Manually set a width. - - When ThreeImageSurface have a width, then they have pretty much the - same API as ImageSurface does. In particular, they can now be nested - in another ThreeImageSurface. - """ - self.width = width - - def get_size(self): - return self.width, self.height - - def draw(self, context, x, y, width, fraction=1.0): - left_width = min(self.left.width, width) - self.left.draw(context, x, y, left_width, self.height, fraction) - self.draw_right(context, x + left_width, y, width - left_width, fraction) - - def draw_right(self, context, x, y, width, fraction=1.0): - # draws only the right two images - - right_width = min(self.right.width, width) - center_width = int(width - right_width) - - self.center.draw(context, x, y, center_width, self.height, fraction) - self.right.draw(context, x + center_width, y, right_width, self.height, fraction) diff --git a/mvc/windows/__init__.py b/mvc/windows/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/mvc/windows/__init__.py +++ /dev/null diff --git a/mvc/windows/autoupdate.py b/mvc/windows/autoupdate.py deleted file mode 100644 index 6264912..0000000 --- a/mvc/windows/autoupdate.py +++ /dev/null @@ -1,101 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2017 -# Jesus E. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -"""Autoupdate functionality """ - -import ctypes -import _winreg as winreg -import logging - -winsparkle = ctypes.cdll.WinSparkle - -APPCAST_URL = 'http://miro-updates.participatoryculture.org/mvc-appcast.xml' - -def startup(): - enable_automatic_checks() - winsparkle.win_sparkle_set_appcast_url(APPCAST_URL) - winsparkle.win_sparkle_init() - -def shutdown(): - winsparkle.win_sparkle_cleanup() - -def enable_automatic_checks(): - # We should be able to use win_sparkle_set_automatic_check_for_updates, - # but that's only available after version 0.4 and the current release - # version is 0.4 - with open_winsparkle_key() as winsparkle_key: - if not check_for_updates_set(winsparkle_key): - set_default_check_for_updates(winsparkle_key) - -def open_winsparkle_key(): - """Open the MVC WinSparkle registry key - - If any components are not created yet, we will try to create them - """ - with open_or_create_key(winreg.HKEY_CURRENT_USER, "Software") as software: - with open_or_create_key(software, - "Participatory Culture Foundation") as pcf: - with open_or_create_key(pcf, "Libre Video Converter") as mvc: - return open_or_create_key(mvc, "WinSparkle", - write_access=True) - -def open_or_create_key(key, subkey, write_access=False): - if write_access: - sam = winreg.KEY_READ | winreg.KEY_WRITE - else: - sam = winreg.KEY_READ - try: - return winreg.OpenKey(key, subkey, 0, sam) - except OSError, e: - if e.errno == 2: - # Not Found error. We should create the key - return winreg.CreateKey(key, subkey) - else: - raise - -def check_for_updates_set(winsparkle_key): - try: - winreg.QueryValueEx(winsparkle_key, "CheckForUpdates") - except OSError, e: - if e.errno == 2: - # not found error. - return False - else: - raise - else: - return True - - -def set_default_check_for_updates(winsparkle_key): - """Initialize the WinSparkle regstry values with our defaults. - - :param mvc_key winreg.HKey object for to the MVC registry - """ - logging.info("Writing WinSparkle keys") - winreg.SetValueEx(winsparkle_key, "CheckForUpdates", 0, winreg.REG_SZ, "1") diff --git a/mvc/windows/exe_main.py b/mvc/windows/exe_main.py deleted file mode 100755 index bd171d3..0000000 --- a/mvc/windows/exe_main.py +++ /dev/null @@ -1,22 +0,0 @@ -# before anything else, settup logging -from mvc.windows import exelogging -exelogging.setup_logging() - -import os -import sys - -from mvc import settings -from mvc.windows import autoupdate -from mvc.widgets import app -from mvc.widgets import initialize -from mvc.ui.widgets import Application - -# add the directories for ffmpeg and avconv to our search path -exe_dir = os.path.dirname(sys.executable) -settings.add_to_search_path(os.path.join(exe_dir, 'ffmpeg')) -settings.add_to_search_path(os.path.join(exe_dir, 'avconv')) -# run the app -app.widgetapp = Application() -app.widgetapp.connect("window-shown", lambda w: autoupdate.startup()) -initialize(app.widgetapp) -autoupdate.shutdown() diff --git a/mvc/windows/exelogging.py b/mvc/windows/exelogging.py deleted file mode 100644 index a90fbfc..0000000 --- a/mvc/windows/exelogging.py +++ /dev/null @@ -1,91 +0,0 @@ -"""mvc.windows.exelogging -- handle logging inside an exe file - -Most of this is copied from the Miro code. -""" - -import logging -import os -import sys -import tempfile -from StringIO import StringIO -from logging.handlers import RotatingFileHandler - -class ApatheticRotatingFileHandler(RotatingFileHandler): - """The whole purpose of this class is to prevent rotation errors - from percolating up into stdout/stderr and popping up a dialog - that's not particularly useful to users or us. - """ - def doRollover(self): - # If you shut down LibreVideoConverter then start it up again immediately - # afterwards, then we get in this squirrely situation where - # the log is opened by another process. We ignore the - # exception, but make sure we have an open file. (bug #11228) - try: - RotatingFileHandler.doRollover(self) - except WindowsError: - if not self.stream or self.stream.closed: - self.stream = open(self.baseFilename, "a") - try: - RotatingFileHandler.doRollover(self) - except WindowsError: - pass - - def shouldRollover(self, record): - # if doRollover doesn't work, then we don't want to find - # ourselves in a situation where we're trying to do things on - # a closed stream. - if self.stream.closed: - self.stream = open(self.baseFilename, "a") - return RotatingFileHandler.shouldRollover(self, record) - - def handleError(self, record): - # ignore logging errors that occur rather than printing them to - # stdout/stderr which isn't helpful to us - - pass -class AutoLoggingStream(StringIO): - """Create a stream that intercepts write calls and sends them to - the log. - """ - def __init__(self, logging_callback, prefix): - StringIO.__init__(self) - # We init from StringIO to give us a bunch of stream-related - # methods, like closed() and read() automatically. - self.logging_callback = logging_callback - self.prefix = prefix - - def write(self, data): - if isinstance(data, unicode): - data = data.encode('ascii', 'backslashreplace') - if data.endswith("\n"): - data = data[:-1] - if data: - self.logging_callback(self.prefix + data) - -FORMAT = "%(asctime)s %(levelname)-8s %(name)s: %(message)s" -def setup_logging(): - """Setup logging for when we're running inside a windows exe. - - The object here is to avoid logging anything to stderr since - windows will consider that an error. - - We also catch things written to sys.stdout and forward that to the logging - system. - - Finally we also copy the log output to stdout so that when MVC is run in - console mode we see the logs - """ - - log_path = os.path.join(tempfile.gettempdir(), "MVC.log") - rotater = ApatheticRotatingFileHandler( - log_path, mode="a", maxBytes=100000, backupCount=5) - - formatter = logging.Formatter(FORMAT) - rotater.setFormatter(formatter) - logger = logging.getLogger('') - logger.addHandler(rotater) - logger.addHandler(logging.StreamHandler(sys.stdout)) - logger.setLevel(logging.INFO) - rotater.doRollover() - sys.stdout = AutoLoggingStream(logging.warn, '(from stdout) ') - sys.stderr = AutoLoggingStream(logging.error, '(from stderr) ') diff --git a/mvc/windows/specialfolders.py b/mvc/windows/specialfolders.py deleted file mode 100644 index 2e1e7c6..0000000 --- a/mvc/windows/specialfolders.py +++ /dev/null @@ -1,94 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Participatory Culture Foundation -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. - -"""Contains the locations of special windows folders like "My -Documents". -""" - -import ctypes -import os -# from miro import u3info - -GetShortPathName = ctypes.windll.kernel32.GetShortPathNameW - -_special_folder_CSIDLs = { - "Fonts": 0x0014, - "AppData": 0x001a, - "My Music": 0x000d, - "My Pictures": 0x0027, - "My Videos": 0x000e, - "My Documents": 0x0005, - "Desktop": 0x0000, - "Common AppData": 0x0023, - "System": 0x0025 -} - -def get_short_path_name(name): - """Given a path, returns the shortened path name. - """ - buf = ctypes.c_wchar_p(name) - buf2 = ctypes.create_unicode_buffer(1024) - - if GetShortPathName(buf, buf2, 1024): - return buf2.value - else: - return buf.value - -def get_special_folder(name): - """Get the location of a special folder. name should be one of - the following: 'AppData', 'My Music', 'My Pictures', 'My Videos', - 'My Documents', 'Desktop'. - - The path to the folder will be returned, or None if the lookup - fails - """ - try: - csidl = _special_folder_CSIDLs[name] - except KeyError: - # FIXME - this will silently fail if the dev did a typo - # for the path name. e.g. My Musc - return None - - buf = ctypes.create_unicode_buffer(260) - SHGetSpecialFolderPath = ctypes.windll.shell32.SHGetSpecialFolderPathW - if SHGetSpecialFolderPath(None, buf, csidl, False): - return buf.value - else: - return None - -common_app_data_directory = get_special_folder("Common AppData") -app_data_directory = get_special_folder("AppData") - -base_movies_directory = get_special_folder('My Videos') -non_video_directory = get_special_folder('Desktop') -# The "My Videos" folder isn't guaranteed to be listed. If it isn't -# there, we do this hack. -if base_movies_directory is None: - base_movies_directory = os.path.join( - get_special_folder('My Documents'), 'My Videos') |