5
# Copyright (c) 2010, Brandon Lewis <brandon_lewis@berkeley.edu>
7
# This program is free software; you can redistribute it and/or
8
# modify it under the terms of the GNU Lesser General Public
9
# License as published by the Free Software Foundation; either
10
# version 2.1 of the License, or (at your option) any later version.
12
# This program is distributed in the hope that it will be useful,
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15
# Lesser General Public License for more details.
17
# You should have received a copy of the GNU Lesser General Public
18
# License along with this program; if not, write to the
19
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
20
# Boston, MA 02110-1301, USA.
24
from gi.repository import Gst
25
from gi.repository import Gtk
28
from gettext import gettext as _
30
from pitivi.render import CachedEncoderList
31
from pitivi.settings import xdg_data_home
32
from pitivi.utils.misc import isWritable
33
from pitivi.configure import get_renderpresets_dir, get_audiopresets_dir, get_videopresets_dir
34
from pitivi.utils import system
37
class DuplicatePresetNameException(Exception):
38
"""Raised when an operation would result in a duplicated preset name."""
42
class PresetManager(object):
43
"""Abstract class for storing a list of presets.
45
Subclasses must provide a filename attribute.
47
@cvar filename: The name of the file where the presets will be stored.
50
@ivar cur_preset: The currently selected preset. Note that a preset has to
51
be selected before it can be changed.
53
@ivar ordered: A list holding (name -> preset_dict) tuples.
54
@type ordered: Gtk.ListStore
55
@ivar presets: A (name -> preset_dict) map.
57
@ivar widget_map: A (propname -> (setter_func, getter_func)) map.
58
These two functions are used when showing or saving a preset.
59
@type widget_map: dict
65
self.ordered = Gtk.ListStore(str, object)
66
self.cur_preset = None
67
# Whether to ignore the updateValue calls.
68
self._ignore_update_requests = False
69
self.system = system.getSystem()
74
for uri in os.listdir(self.default_path):
75
filepaths.append(os.path.join(self.default_path, uri))
79
for uri in os.listdir(self.user_path):
80
filepaths.append(os.path.join(self.user_path, uri))
84
for filepath in filepaths:
85
if filepath.endswith("json"):
86
self._loadSection(os.path.join(self.default_path, filepath))
89
"""Write changes to disk for all presets"""
90
for preset_name, values in self.ordered:
91
self.savePreset(preset_name)
93
def savePreset(self, preset_name):
94
if preset_name == _("No preset"):
96
if os.path.isfile(self.user_path):
97
# We used to save presets as a single file instead of a directory
98
os.remove(self.user_path)
99
if not os.path.exists(self.user_path):
100
os.makedirs(self.user_path)
102
file_path = self.presets[preset_name]["filepath"]
104
file_name = self.system.getUniqueFilename(preset_name + ".json")
105
file_path = os.path.join(self.user_path, file_name)
106
self.presets[preset_name]["filepath"] = file_path
108
with open(file_path, "w") as fout:
109
self._saveSection(fout, preset_name)
111
# FIXME: this can happen in two cases: a permissions error,
112
# or an invalid filename (ex: gibberish). In the latter case
113
# we should log an error message or show an infobar, and
114
# the test suite should verify this
117
def _convertSectionNameToPresetName(self, section):
118
# A section name for a ConfigParser can have any name except "default"!
119
assert section != "default"
120
if section.rstrip("_").lower() == "default":
125
def _convertPresetNameToSectionName(self, preset):
126
if preset.rstrip("_").lower() == "default":
127
# We add an _ to allow the user to have a preset named "default".
128
return "%s_" % preset
132
def addPreset(self, name, values):
135
@param name: The name of the new preset.
137
@param values: The values of the new preset.
140
if type(name) == unicode:
141
# We need utf-8 string objects, not unicode objects!
142
name = name.encode("utf-8")
143
if self.hasPreset(name):
144
raise DuplicatePresetNameException(name)
145
self.presets[name] = values
146
# Note: This generates a "row-inserted" signal in the model.
147
self.ordered.append((name, values))
149
def renamePreset(self, path, new_name):
150
"""Change the name of a preset.
152
@param path: The path in the model identifying the preset to be renamed.
154
@param new_name: The new name for the preset.
157
old_name = self.ordered[path][0]
158
assert old_name in self.presets
159
if old_name == new_name:
162
if old_name.lower() != new_name.lower() and self.hasPreset(new_name):
163
raise DuplicatePresetNameException()
164
self.ordered[path][0] = new_name
165
self.presets[new_name] = self.presets[old_name]
166
if "filepath" in self.presets[old_name]:
167
# If the previous preset had already been saved,
168
# delete the file and pop it from the list
169
self.removePreset(old_name)
171
# We're renaming an unsaved preset, so just pop it from the list
172
self.presets.pop(old_name)
173
new_filepath = os.path.join(self.user_path, new_name + ".json")
174
self.presets[new_name]["filepath"] = new_filepath
175
self.cur_preset = new_name
176
self.saveCurrentPreset()
178
def hasPreset(self, name):
180
return any(name == preset.lower() for preset in self.getPresetNames())
182
def getPresetNames(self):
183
return (row[0] for row in self.ordered)
186
"""Get the GtkModel used by the UI."""
189
def updateValue(self, name, value):
190
"""Update a value in the current preset, if any."""
191
if self._ignore_update_requests:
192
# This is caused by restorePreset, nothing to do.
195
self.presets[self.cur_preset][name] = value
197
def bindWidget(self, propname, setter_func, getter_func):
198
"""Link the specified functions to the specified preset property."""
199
self.widget_map[propname] = (setter_func, getter_func)
201
def restorePreset(self, preset):
202
"""Select a preset and copy the values from the preset to the widgets.
204
@param preset: The name of the preset to be selected.
207
self._ignore_update_requests = True
209
self.cur_preset = None
211
elif not preset in self.presets:
213
values = self.presets[preset]
214
self.cur_preset = preset
215
for field, (setter, getter) in self.widget_map.iteritems():
216
if values[field] != 0:
217
setter(values[field])
219
setter(self.presets[_("No preset")][field])
220
self._ignore_update_requests = False
222
def saveCurrentPreset(self):
223
"""Update the current preset values from the widgets and save it."""
224
if self.cur_preset != _("No preset"):
226
self.savePreset(self.cur_preset)
228
def _updatePreset(self):
229
"""Copy the values from the widgets to the preset."""
230
values = self.presets[self.cur_preset]
231
for field, (setter, getter) in self.widget_map.iteritems():
232
values[field] = getter()
234
def _isCurrentPresetChanged(self):
235
"""Return whether the widgets values differ from those of the preset."""
236
if not self.cur_preset:
237
# There is no preset selected, nothing to do.
239
values = self.presets[self.cur_preset]
240
return any((values[field] != getter()
241
for field, (setter, getter) in self.widget_map.iteritems()))
243
def removePreset(self, name):
245
os.remove(self.presets[name]["filepath"]) # Deletes json file if exists
247
# Trying to remove a preset that has not actually been saved
251
self.presets.pop(name)
252
for i, row in enumerate(self.ordered):
256
if self.cur_preset == name:
257
self.cur_preset = None
259
def prependPreset(self, name, values):
260
self.presets[name] = values
261
# Note: This generates a "row-inserted" signal in the model.
262
self.ordered.prepend((name, values))
264
def isSaveButtonSensitive(self):
265
"""Check if the Save button should be sensitive"""
266
if not self.cur_preset or self.cur_preset == _("No preset"):
269
full_path = self.presets[self.cur_preset]["filepath"]
270
(dir, name) = os.path.split(full_path)
272
# This is a newly created preset that has not yet been saved
274
if dir == self.default_path or not isWritable(full_path):
275
# default_path is the system-wide directory where the default
276
# presets are installed; they are not expected to be editable.
279
return self._isCurrentPresetChanged()
281
def isRemoveButtonSensitive(self):
282
"""Check if Remove buttons should be sensitive"""
283
if not self.cur_preset or self.cur_preset == _("No preset"):
286
full_path = self.presets[self.cur_preset]["filepath"]
287
(dir, name) = os.path.split(full_path)
289
# This is a newly created preset that has not yet been saved
290
# We cannot remove it since it does not exist
292
if dir == self.default_path or not isWritable(full_path):
293
# default_path is the system-wide directory where the default
294
# presets are installed; they are not expected to be editable.
300
def _saveSection(self, fout, section):
301
"""Save the specified section into the specified file.
303
@param fout: The file where to save the section.
305
@param section: The name of the section to be saved.
306
@type section: string
308
raise NotImplementedError()
311
class VideoPresetManager(PresetManager):
313
default_path = get_videopresets_dir()
314
user_path = os.path.join(xdg_data_home(), 'video_presets')
316
def _loadSection(self, filepath):
317
parser = json.loads(open(filepath).read())
319
name = parser["name"]
320
width = parser["width"]
321
height = parser["height"]
323
framerate_num = parser["framerate-num"]
324
framerate_denom = parser["framerate-denom"]
325
framerate = Gst.Fraction(framerate_num, framerate_denom)
327
par_num = parser["par-num"]
328
par_denom = parser["par-denom"]
329
par = Gst.Fraction(par_num, par_denom)
331
self.addPreset(name, {
334
"frame-rate": framerate,
336
"filepath": filepath,
339
def _saveSection(self, fout, section):
340
values = self.presets[section]
343
"width": int(values["width"]),
344
"height": int(values["height"]),
345
"framerate-num": values["frame-rate"].num,
346
"framerate-denom": values["frame-rate"].denom,
347
"par-num": values["par"].num,
348
"par-denom": values["par"].denom,
353
class AudioPresetManager(PresetManager):
355
default_path = get_audiopresets_dir()
356
user_path = os.path.join(xdg_data_home(), 'audio_presets')
358
def _loadSection(self, filepath):
359
parser = json.loads(open(filepath).read())
361
name = parser["name"]
363
channels = parser["channels"]
364
sample_rate = parser["sample-rate"]
366
self.addPreset(name, {
367
"channels": channels,
368
"sample-rate": sample_rate,
369
"filepath": filepath,
372
def _saveSection(self, fout, section):
373
values = self.presets[section]
376
"channels": values["channels"],
377
"sample-rate": int(values["sample-rate"]),
382
class RenderPresetManager(PresetManager):
384
default_path = get_renderpresets_dir()
385
user_path = os.path.join(xdg_data_home(), 'render_presets')
387
def _loadSection(self, filepath):
388
parser = json.loads(open(filepath).read())
390
name = parser["name"]
391
container = parser["container"]
392
acodec = parser["acodec"]
393
vcodec = parser["vcodec"]
395
cached_encs = CachedEncoderList()
396
if (acodec not in [fact.get_name() for fact in cached_encs.aencoders]
397
or vcodec not in [fact.get_name() for fact in cached_encs.vencoders]
398
or container not in [fact.get_name() for fact in cached_encs.muxers]):
402
width = parser["width"]
403
height = parser["height"]
408
framerate_num = parser["framerate-num"]
409
framerate_denom = parser["framerate-denom"]
410
framerate = Gst.Fraction(framerate_num, framerate_denom)
412
channels = parser["channels"]
413
sample_rate = parser["sample-rate"]
415
self.addPreset(name, {
416
"container": container,
421
"frame-rate": framerate,
422
"channels": channels,
423
"sample-rate": sample_rate,
424
"filepath": filepath,
427
def _saveSection(self, fout, section):
428
values = self.presets[section]
431
"container": str(values["container"]),
432
"acodec": str(values["acodec"]),
433
"vcodec": str(values["vcodec"]),
434
"width": int(values["width"]),
435
"height": int(values["height"]),
436
"framerate-num": values["frame-rate"].num,
437
"framerate-denom": values["frame-rate"].denom,
438
"channels": values["channels"],
439
"sample-rate": int(values["sample-rate"]),