aboutsummaryrefslogtreecommitdiffstats
path: root/mvc
diff options
context:
space:
mode:
Diffstat (limited to 'mvc')
-rw-r--r--mvc/__init__.py37
-rw-r--r--mvc/__main__.py9
-rw-r--r--mvc/basicconverters.py141
-rw-r--r--mvc/conversion.py313
-rw-r--r--mvc/converter.py278
-rw-r--r--mvc/errors.py89
-rw-r--r--mvc/execute.py49
-rw-r--r--mvc/openfiles.py46
-rw-r--r--mvc/osx/__init__.py0
-rw-r--r--mvc/osx/app_main.py12
-rw-r--r--mvc/osx/autoupdate.py9
-rw-r--r--mvc/qtfaststart/__init__.py1
-rw-r--r--mvc/qtfaststart/exceptions.py5
-rwxr-xr-xmvc/qtfaststart/processor.py215
-rw-r--r--mvc/resources/__init__.py21
-rw-r--r--mvc/resources/converters/android.py61
-rw-r--r--mvc/resources/converters/apple.py28
-rw-r--r--mvc/resources/converters/others.py20
-rw-r--r--mvc/resources/images/android-icon-off.pngbin783 -> 0 bytes
-rw-r--r--mvc/resources/images/android-icon-on.pngbin423 -> 0 bytes
-rw-r--r--mvc/resources/images/apple-icon-off.pngbin426 -> 0 bytes
-rw-r--r--mvc/resources/images/apple-icon-on.pngbin341 -> 0 bytes
-rw-r--r--mvc/resources/images/arrow-down-off.pngbin132 -> 0 bytes
-rw-r--r--mvc/resources/images/arrow-down-on.pngbin116 -> 0 bytes
-rw-r--r--mvc/resources/images/audio.pngbin1257 -> 0 bytes
-rw-r--r--mvc/resources/images/clear-icon.pngbin252 -> 0 bytes
-rw-r--r--mvc/resources/images/convert-button-off.pngbin1005 -> 0 bytes
-rw-r--r--mvc/resources/images/convert-button-on.pngbin1242 -> 0 bytes
-rw-r--r--mvc/resources/images/convert-button-stop.pngbin1050 -> 0 bytes
-rw-r--r--mvc/resources/images/converted_to-icon.pngbin253 -> 0 bytes
-rw-r--r--mvc/resources/images/dropoff-icon-off.pngbin1413 -> 0 bytes
-rw-r--r--mvc/resources/images/dropoff-icon-on.pngbin1183 -> 0 bytes
-rw-r--r--mvc/resources/images/dropoff-icon-small-off.pngbin1321 -> 0 bytes
-rw-r--r--mvc/resources/images/dropoff-icon-small-on.pngbin1080 -> 0 bytes
-rw-r--r--mvc/resources/images/error-icon.pngbin267 -> 0 bytes
-rw-r--r--mvc/resources/images/item-completed.pngbin790 -> 0 bytes
-rw-r--r--mvc/resources/images/item-delete-button-off.pngbin872 -> 0 bytes
-rw-r--r--mvc/resources/images/item-delete-button-on.pngbin988 -> 0 bytes
-rw-r--r--mvc/resources/images/item-error.pngbin829 -> 0 bytes
-rw-r--r--mvc/resources/images/mvc-logo.pngbin2243 -> 0 bytes
-rw-r--r--mvc/resources/images/other-icon-off.pngbin244 -> 0 bytes
-rw-r--r--mvc/resources/images/other-icon-on.pngbin310 -> 0 bytes
-rw-r--r--mvc/resources/images/progressbar-base.pngbin324 -> 0 bytes
-rw-r--r--mvc/resources/images/queued-icon.pngbin112 -> 0 bytes
-rw-r--r--mvc/resources/images/settings-base_center.pngbin134 -> 0 bytes
-rw-r--r--mvc/resources/images/settings-base_left.pngbin325 -> 0 bytes
-rw-r--r--mvc/resources/images/settings-base_right.pngbin345 -> 0 bytes
-rw-r--r--mvc/resources/images/settings-depth_center.pngbin92 -> 0 bytes
-rw-r--r--mvc/resources/images/settings-depth_left.pngbin267 -> 0 bytes
-rw-r--r--mvc/resources/images/settings-depth_right.pngbin260 -> 0 bytes
-rw-r--r--mvc/resources/images/settings-dropdown-bottom-bg.pngbin1203 -> 0 bytes
-rw-r--r--mvc/resources/images/settings-icon-off.pngbin496 -> 0 bytes
-rw-r--r--mvc/resources/images/settings-icon-on.pngbin422 -> 0 bytes
-rw-r--r--mvc/resources/images/showfile-icon.pngbin243 -> 0 bytes
-rw-r--r--mvc/resources/nsis/modern-wizard.bmpbin154542 -> 0 bytes
-rw-r--r--mvc/resources/nsis/mvc-logo.icobin15086 -> 0 bytes
-rw-r--r--mvc/resources/nsis/plugins/nsProcess.dllbin4096 -> 0 bytes
-rw-r--r--mvc/resources/nsis/plugins/nsProcess.nsh21
-rw-r--r--mvc/resources/windows/README7
-rwxr-xr-xmvc/resources/windows/gtkrc182
-rw-r--r--mvc/settings.py88
-rw-r--r--mvc/signals.py299
-rw-r--r--mvc/ui/__init__.py0
-rw-r--r--mvc/ui/console.py120
-rw-r--r--mvc/ui/widgets.py1540
-rw-r--r--mvc/utils.py230
-rw-r--r--mvc/video.py287
-rw-r--r--mvc/widgets/__init__.py30
-rw-r--r--mvc/widgets/app.py4
-rw-r--r--mvc/widgets/cellpack.py843
-rw-r--r--mvc/widgets/dialogs.py276
-rw-r--r--mvc/widgets/gtk/__init__.py65
-rw-r--r--mvc/widgets/gtk/base.py300
-rw-r--r--mvc/widgets/gtk/const.py44
-rw-r--r--mvc/widgets/gtk/contextmenu.py31
-rw-r--r--mvc/widgets/gtk/controls.py337
-rw-r--r--mvc/widgets/gtk/customcontrols.py517
-rw-r--r--mvc/widgets/gtk/drawing.py268
-rw-r--r--mvc/widgets/gtk/gtkmenus.py404
-rw-r--r--mvc/widgets/gtk/keymap.py94
-rw-r--r--mvc/widgets/gtk/layout.py227
-rw-r--r--mvc/widgets/gtk/layoutmanager.py550
-rw-r--r--mvc/widgets/gtk/simple.py313
-rw-r--r--mvc/widgets/gtk/tableview.py1557
-rw-r--r--mvc/widgets/gtk/tableviewcells.py249
-rw-r--r--mvc/widgets/gtk/weakconnect.py56
-rw-r--r--mvc/widgets/gtk/widgets.py47
-rw-r--r--mvc/widgets/gtk/widgetset.py63
-rw-r--r--mvc/widgets/gtk/window.py708
-rw-r--r--mvc/widgets/gtk/wrappermap.py50
-rw-r--r--mvc/widgets/keyboard.py69
-rw-r--r--mvc/widgets/menus.py268
-rw-r--r--mvc/widgets/osx/Resources-Widgets/MainMenu.nib/designable.nib145
-rw-r--r--mvc/widgets/osx/Resources-Widgets/MainMenu.nib/keyedobjects.nibbin1609 -> 0 bytes
-rw-r--r--mvc/widgets/osx/__init__.py74
-rw-r--r--mvc/widgets/osx/base.py367
-rw-r--r--mvc/widgets/osx/const.py44
-rw-r--r--mvc/widgets/osx/contextmenu.py84
-rw-r--r--mvc/widgets/osx/control.py530
-rw-r--r--mvc/widgets/osx/customcontrol.py436
-rw-r--r--mvc/widgets/osx/drawing.py289
-rw-r--r--mvc/widgets/osx/drawingwidgets.py67
-rw-r--r--mvc/widgets/osx/fasttypes.c540
-rw-r--r--mvc/widgets/osx/helpers.py95
-rw-r--r--mvc/widgets/osx/layout.py748
-rw-r--r--mvc/widgets/osx/layoutmanager.py445
-rw-r--r--mvc/widgets/osx/osxmenus.py571
-rw-r--r--mvc/widgets/osx/rect.py78
-rw-r--r--mvc/widgets/osx/simple.py376
-rw-r--r--mvc/widgets/osx/tablemodel.py532
-rw-r--r--mvc/widgets/osx/tableview.py1629
-rw-r--r--mvc/widgets/osx/utils.py2
-rw-r--r--mvc/widgets/osx/viewport.py101
-rw-r--r--mvc/widgets/osx/widgetset.py58
-rw-r--r--mvc/widgets/osx/widgetupdates.py72
-rw-r--r--mvc/widgets/osx/window.py896
-rw-r--r--mvc/widgets/osx/wrappermap.py48
-rw-r--r--mvc/widgets/tablescroll.py154
-rw-r--r--mvc/widgets/tableselection.py220
-rw-r--r--mvc/widgets/widgetconst.py44
-rw-r--r--mvc/widgets/widgetutil.py225
-rw-r--r--mvc/windows/__init__.py0
-rw-r--r--mvc/windows/autoupdate.py101
-rwxr-xr-xmvc/windows/exe_main.py22
-rw-r--r--mvc/windows/exelogging.py91
-rw-r--r--mvc/windows/specialfolders.py94
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
deleted file mode 100644
index 5948f4c..0000000
--- a/mvc/resources/images/android-icon-off.png
+++ /dev/null
Binary files differ
diff --git a/mvc/resources/images/android-icon-on.png b/mvc/resources/images/android-icon-on.png
deleted file mode 100644
index 85be5be..0000000
--- a/mvc/resources/images/android-icon-on.png
+++ /dev/null
Binary files differ
diff --git a/mvc/resources/images/apple-icon-off.png b/mvc/resources/images/apple-icon-off.png
deleted file mode 100644
index 947bfae..0000000
--- a/mvc/resources/images/apple-icon-off.png
+++ /dev/null
Binary files differ
diff --git a/mvc/resources/images/apple-icon-on.png b/mvc/resources/images/apple-icon-on.png
deleted file mode 100644
index 9949653..0000000
--- a/mvc/resources/images/apple-icon-on.png
+++ /dev/null
Binary files differ
diff --git a/mvc/resources/images/arrow-down-off.png b/mvc/resources/images/arrow-down-off.png
deleted file mode 100644
index 368079f..0000000
--- a/mvc/resources/images/arrow-down-off.png
+++ /dev/null
Binary files differ
diff --git a/mvc/resources/images/arrow-down-on.png b/mvc/resources/images/arrow-down-on.png
deleted file mode 100644
index 8963b5b..0000000
--- a/mvc/resources/images/arrow-down-on.png
+++ /dev/null
Binary files differ
diff --git a/mvc/resources/images/audio.png b/mvc/resources/images/audio.png
deleted file mode 100644
index 4d59605..0000000
--- a/mvc/resources/images/audio.png
+++ /dev/null
Binary files differ
diff --git a/mvc/resources/images/clear-icon.png b/mvc/resources/images/clear-icon.png
deleted file mode 100644
index 5b054fa..0000000
--- a/mvc/resources/images/clear-icon.png
+++ /dev/null
Binary files differ
diff --git a/mvc/resources/images/convert-button-off.png b/mvc/resources/images/convert-button-off.png
deleted file mode 100644
index 307a8bd..0000000
--- a/mvc/resources/images/convert-button-off.png
+++ /dev/null
Binary files differ
diff --git a/mvc/resources/images/convert-button-on.png b/mvc/resources/images/convert-button-on.png
deleted file mode 100644
index 2a66c76..0000000
--- a/mvc/resources/images/convert-button-on.png
+++ /dev/null
Binary files differ
diff --git a/mvc/resources/images/convert-button-stop.png b/mvc/resources/images/convert-button-stop.png
deleted file mode 100644
index cb09a97..0000000
--- a/mvc/resources/images/convert-button-stop.png
+++ /dev/null
Binary files differ
diff --git a/mvc/resources/images/converted_to-icon.png b/mvc/resources/images/converted_to-icon.png
deleted file mode 100644
index 14ee6d3..0000000
--- a/mvc/resources/images/converted_to-icon.png
+++ /dev/null
Binary files differ
diff --git a/mvc/resources/images/dropoff-icon-off.png b/mvc/resources/images/dropoff-icon-off.png
deleted file mode 100644
index e182d49..0000000
--- a/mvc/resources/images/dropoff-icon-off.png
+++ /dev/null
Binary files differ
diff --git a/mvc/resources/images/dropoff-icon-on.png b/mvc/resources/images/dropoff-icon-on.png
deleted file mode 100644
index 1dfd88f..0000000
--- a/mvc/resources/images/dropoff-icon-on.png
+++ /dev/null
Binary files differ
diff --git a/mvc/resources/images/dropoff-icon-small-off.png b/mvc/resources/images/dropoff-icon-small-off.png
deleted file mode 100644
index 186a7e6..0000000
--- a/mvc/resources/images/dropoff-icon-small-off.png
+++ /dev/null
Binary files differ
diff --git a/mvc/resources/images/dropoff-icon-small-on.png b/mvc/resources/images/dropoff-icon-small-on.png
deleted file mode 100644
index 476ea49..0000000
--- a/mvc/resources/images/dropoff-icon-small-on.png
+++ /dev/null
Binary files differ
diff --git a/mvc/resources/images/error-icon.png b/mvc/resources/images/error-icon.png
deleted file mode 100644
index 656b2c3..0000000
--- a/mvc/resources/images/error-icon.png
+++ /dev/null
Binary files differ
diff --git a/mvc/resources/images/item-completed.png b/mvc/resources/images/item-completed.png
deleted file mode 100644
index 1400eda..0000000
--- a/mvc/resources/images/item-completed.png
+++ /dev/null
Binary files differ
diff --git a/mvc/resources/images/item-delete-button-off.png b/mvc/resources/images/item-delete-button-off.png
deleted file mode 100644
index 12cd239..0000000
--- a/mvc/resources/images/item-delete-button-off.png
+++ /dev/null
Binary files differ
diff --git a/mvc/resources/images/item-delete-button-on.png b/mvc/resources/images/item-delete-button-on.png
deleted file mode 100644
index 45786e5..0000000
--- a/mvc/resources/images/item-delete-button-on.png
+++ /dev/null
Binary files differ
diff --git a/mvc/resources/images/item-error.png b/mvc/resources/images/item-error.png
deleted file mode 100644
index 710ff61..0000000
--- a/mvc/resources/images/item-error.png
+++ /dev/null
Binary files differ
diff --git a/mvc/resources/images/mvc-logo.png b/mvc/resources/images/mvc-logo.png
deleted file mode 100644
index fce15e4..0000000
--- a/mvc/resources/images/mvc-logo.png
+++ /dev/null
Binary files differ
diff --git a/mvc/resources/images/other-icon-off.png b/mvc/resources/images/other-icon-off.png
deleted file mode 100644
index a6c76f2..0000000
--- a/mvc/resources/images/other-icon-off.png
+++ /dev/null
Binary files differ
diff --git a/mvc/resources/images/other-icon-on.png b/mvc/resources/images/other-icon-on.png
deleted file mode 100644
index 6c60edc..0000000
--- a/mvc/resources/images/other-icon-on.png
+++ /dev/null
Binary files differ
diff --git a/mvc/resources/images/progressbar-base.png b/mvc/resources/images/progressbar-base.png
deleted file mode 100644
index 298a6b6..0000000
--- a/mvc/resources/images/progressbar-base.png
+++ /dev/null
Binary files differ
diff --git a/mvc/resources/images/queued-icon.png b/mvc/resources/images/queued-icon.png
deleted file mode 100644
index d4e9242..0000000
--- a/mvc/resources/images/queued-icon.png
+++ /dev/null
Binary files differ
diff --git a/mvc/resources/images/settings-base_center.png b/mvc/resources/images/settings-base_center.png
deleted file mode 100644
index d5f3065..0000000
--- a/mvc/resources/images/settings-base_center.png
+++ /dev/null
Binary files differ
diff --git a/mvc/resources/images/settings-base_left.png b/mvc/resources/images/settings-base_left.png
deleted file mode 100644
index a0f10c2..0000000
--- a/mvc/resources/images/settings-base_left.png
+++ /dev/null
Binary files differ
diff --git a/mvc/resources/images/settings-base_right.png b/mvc/resources/images/settings-base_right.png
deleted file mode 100644
index 14456eb..0000000
--- a/mvc/resources/images/settings-base_right.png
+++ /dev/null
Binary files differ
diff --git a/mvc/resources/images/settings-depth_center.png b/mvc/resources/images/settings-depth_center.png
deleted file mode 100644
index fb5f586..0000000
--- a/mvc/resources/images/settings-depth_center.png
+++ /dev/null
Binary files differ
diff --git a/mvc/resources/images/settings-depth_left.png b/mvc/resources/images/settings-depth_left.png
deleted file mode 100644
index a13694b..0000000
--- a/mvc/resources/images/settings-depth_left.png
+++ /dev/null
Binary files differ
diff --git a/mvc/resources/images/settings-depth_right.png b/mvc/resources/images/settings-depth_right.png
deleted file mode 100644
index 5ddd21f..0000000
--- a/mvc/resources/images/settings-depth_right.png
+++ /dev/null
Binary files differ
diff --git a/mvc/resources/images/settings-dropdown-bottom-bg.png b/mvc/resources/images/settings-dropdown-bottom-bg.png
deleted file mode 100644
index bc650f8..0000000
--- a/mvc/resources/images/settings-dropdown-bottom-bg.png
+++ /dev/null
Binary files differ
diff --git a/mvc/resources/images/settings-icon-off.png b/mvc/resources/images/settings-icon-off.png
deleted file mode 100644
index 340b516..0000000
--- a/mvc/resources/images/settings-icon-off.png
+++ /dev/null
Binary files differ
diff --git a/mvc/resources/images/settings-icon-on.png b/mvc/resources/images/settings-icon-on.png
deleted file mode 100644
index be008d4..0000000
--- a/mvc/resources/images/settings-icon-on.png
+++ /dev/null
Binary files differ
diff --git a/mvc/resources/images/showfile-icon.png b/mvc/resources/images/showfile-icon.png
deleted file mode 100644
index 7f9040f..0000000
--- a/mvc/resources/images/showfile-icon.png
+++ /dev/null
Binary files differ
diff --git a/mvc/resources/nsis/modern-wizard.bmp b/mvc/resources/nsis/modern-wizard.bmp
deleted file mode 100644
index d8ea8d9..0000000
--- a/mvc/resources/nsis/modern-wizard.bmp
+++ /dev/null
Binary files differ
diff --git a/mvc/resources/nsis/mvc-logo.ico b/mvc/resources/nsis/mvc-logo.ico
deleted file mode 100644
index 007a929..0000000
--- a/mvc/resources/nsis/mvc-logo.ico
+++ /dev/null
Binary files differ
diff --git a/mvc/resources/nsis/plugins/nsProcess.dll b/mvc/resources/nsis/plugins/nsProcess.dll
deleted file mode 100644
index 4355d4a..0000000
--- a/mvc/resources/nsis/plugins/nsProcess.dll
+++ /dev/null
Binary files differ
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
deleted file mode 100644
index 963b444..0000000
--- a/mvc/widgets/osx/Resources-Widgets/MainMenu.nib/keyedobjects.nib
+++ /dev/null
Binary files differ
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')