11#
2# Entangle: Tethered Camera Control & Capture
3#
4# Copyright (C) 2014 Daniel P. Berrange
5#
6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation, either version 3 of the License, or
9# (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program.  If not, see <http://www.gnu.org/licenses/>.
18#
19
20import time
21
22import gi
23
24from gi.repository import GObject
25gi.require_version("Gtk", "3.0")
26from gi.repository import Gtk
27from gi.repository import Gdk
28from gi.repository import GLib
29from gi.repository import Gio
30from gi.repository import Peas
31gi.require_version("PeasGtk", "1.0")
32from gi.repository import PeasGtk
33gi.require_version("Entangle", "0.1")
34from gi.repository import Entangle
35
36
37ISO_APERTURE_TO_INDEX_MAP = [
38    ["25",   ["1.4", "2",   "2.8", "4",   "5.6", "8",  "11", "16",  "22"]],
39    ["50",   ["2",   "2.8", "4",   "5.6", "8",   "11", "16", "22",  "32"]],
40    ["100",  ["2.8", "4",   "5.6", "8",   "11",  "16", "22", "32",  "44"]],
41    ["200",  ["4",   "5.6", "8",   "11",  "16",  "22", "32", "44",  "64"]],
42    ["400",  ["5.6", "8",   "11",  "16",  "22",  "32", "44", "64",  "88"]],
43    ["800",  ["8",   "11",  "16",  "22",  "32",  "44", "64", "88",  "128"]],
44    ["1600", ["11",  "16",  "22",  "32",  "44",  "64", "88", "128", "176"]],
45]
46
47INDEX_TO_SPEED_MAP = [
48    [
49        "1/2000",
50        "1/500",
51        "1/125",
52        "1/30",
53        "1/15",
54        "1/8",
55        "1/2",
56    ],
57    [
58        "1/1000",
59        "1/250",
60        "1/60",
61        "1/15",
62        "1/8",
63        "1/4",
64        "1",
65    ],
66    [
67        "1/500",
68        "1/125",
69        "1/30",
70        "1/8",
71        "1/4",
72        "1/2",
73        "2",
74    ],
75
76    [
77        "1/250",
78        "1/60",
79        "1/15",
80        "1/4",
81        "1/2",
82        "1",
83        "4",
84    ],
85    [
86        "1/125",
87        "1/30",
88        "1/8",
89        "1/2",
90        "1",
91        "2",
92        "8",
93    ],
94    [
95        "1/60",
96        "1/15",
97        "1/4",
98        "1",
99        "2",
100        "4",
101        "15",
102    ],
103
104    [
105        "1/30",
106        "1/8",
107        "1/2",
108        "2",
109        "4",
110        "8",
111        "30",
112    ],
113    [
114        "1/15",
115        "1/4",
116        "1",
117        "4",
118        "8",
119        "15",
120        "60",
121    ],
122    [
123        "1/8",
124        "1/2",
125        "2",
126        "8",
127        "15",
128        "30",
129        "120",
130    ]
131]
132
133
134class EclipsePluginWidget(Gtk.Grid):
135    '''The form for controlling parameters of
136    the batch mode shooting session and the progress'''
137
138    def do_iso_changed(self, data):
139        self.do_aperture_populate()
140        self.config.set_iso(self.iso.get_active_text())
141
142    def do_aperture_changed(self, data):
143        self.config.set_aperture(self.aperture.get_active_text())
144
145    def do_aperture_populate(self):
146        isoidx = self.iso.get_active()
147        if isoidx == -1:
148            self.aperture.set_sensitive(False)
149        else:
150            self.aperture.set_sensitive(True)
151
152            self.aperture.remove_all()
153            defset = False
154            for apertureidx in range(len(ISO_APERTURE_TO_INDEX_MAP[isoidx][1])):
155                aperture = ISO_APERTURE_TO_INDEX_MAP[isoidx][1][apertureidx]
156                self.aperture.append(aperture, aperture)
157                if aperture == self.config.get_aperture():
158                    self.aperture.set_active(apertureidx)
159                    defset = True
160            if not defset:
161                self.aperture.set_active(0)
162
163    def __init__(self, config):
164        super(EclipsePluginWidget, self).__init__()
165
166        self.config = config
167
168        self.set_properties(column_spacing=6,
169                            row_spacing=6,
170                            expand=False)
171
172        self.attach(Gtk.Label("ISO:"), 0, 0, 1, 1)
173        self.iso = Gtk.ComboBoxText()
174
175        for isoidx in range(len(ISO_APERTURE_TO_INDEX_MAP)):
176            iso = ISO_APERTURE_TO_INDEX_MAP[isoidx][0]
177            self.iso.append(iso, iso)
178            if iso == config.get_iso():
179                self.iso.set_active(isoidx)
180        self.iso.set_properties(expand=True)
181        self.iso.connect("changed", self.do_iso_changed)
182        self.attach(self.iso, 1, 0, 1, 1)
183
184        self.attach(Gtk.Label("Aperture:"), 0, 1, 1, 1)
185        self.aperture = Gtk.ComboBoxText()
186        self.do_aperture_populate()
187        self.aperture.connect("changed", self.do_aperture_changed)
188        self.attach(self.aperture, 1, 1, 1, 1)
189
190        self.show_all()
191
192class EclipsePluginData(GObject.Object):
193
194    __gtype_name__ = "EclipsePluginData"
195
196    def _get_speeds(self, iso, aperture):
197        for isoidx in range(len(ISO_APERTURE_TO_INDEX_MAP)):
198            thisiso = ISO_APERTURE_TO_INDEX_MAP[isoidx][0]
199            if thisiso != iso:
200                continue
201
202            for apertureidx in range(len(ISO_APERTURE_TO_INDEX_MAP[isoidx][1])):
203                thisaperture = ISO_APERTURE_TO_INDEX_MAP[isoidx][1][apertureidx]
204                if thisaperture != aperture:
205                    continue
206
207                return INDEX_TO_SPEED_MAP[apertureidx]
208
209            return []
210
211        return []
212
213    def __init__(self, iso, aperture):
214        super(EclipsePluginData, self).__init__()
215
216        self.speeds = self._get_speeds(iso, aperture)
217        self.speedIndex = 0
218        self.automata = None
219
220    def speed(self):
221        speed = self.speeds[self.speedIndex]
222        self.speedIndex = self.speedIndex + 1
223        return speed
224
225    def finished(self):
226        return self.speedIndex == len(self.speeds)
227
228    def reset(self):
229        self.speedIndex = 0
230
231
232class EclipsePluginScript(Entangle.ScriptSimple):
233    '''The script for controlling the camera'''
234
235    def __init__(self, config):
236        super(EclipsePluginScript, self).__init__(
237            title="Eclipse Totality"
238        )
239
240        self.config = config
241        self.widget = EclipsePluginWidget(config)
242
243    def do_get_config_widget(self):
244        return self.widget
245
246    def do_capture_callback(self, automata, result, script_result):
247        try:
248            automata.capture_finish(result)
249        except GLib.Error as e:
250            self.return_task_error(script_result, str(e));
251            return
252
253        if script_result.return_error_if_cancelled():
254            return
255
256        cancel = script_result.get_cancellable()
257        self.do_next_step(cancel, script_result)
258
259    def do_init_task_data(self):
260        return EclipsePluginData(self.config.get_iso(),
261                                 self.config.get_aperture())
262
263    def do_next_step(self, cancel, script_result):
264        data = self.get_task_data(script_result)
265        if data.finished():
266            script_result.return_boolean(True)
267            return
268
269        automata = script_result.automata
270        camera = automata.get_camera()
271        controls = camera.get_controls()
272        widget = controls.get_by_path("/main/capturesettings/shutterspeed2")
273        speed = data.speed()
274        widget.set_property("value", speed)
275        camera.save_controls_async(cancel, self.do_save_callback, script_result)
276
277    def do_save_callback(self, camera, result, script_result):
278        try:
279            camera.save_controls_finish(result)
280        except GLib.Error as e:
281            self.return_task_error(script_result, str(e));
282            return
283
284        if script_result.return_error_if_cancelled():
285            return
286
287        automata = script_result.automata
288        automata.capture_async(script_result.get_cancellable(),
289                               self.do_capture_callback,
290                               script_result)
291
292    def do_execute(self, automata, cancel, result):
293        result.automata = automata
294
295        self.do_next_step(cancel, result)
296
297
298class EclipsePluginConfig():
299    '''Provides integration with GSettings to read/write
300    configuration parameters'''
301
302    def __init__(self, plugin_info):
303        settingsdir = plugin_info.get_data_dir() + "/schemas"
304        sssdef = Gio.SettingsSchemaSource.get_default()
305        sss = Gio.SettingsSchemaSource.new_from_directory(settingsdir, sssdef, False)
306        schema = sss.lookup("org.entangle-photo.plugins.eclipse", False)
307        self.settings = Gio.Settings.new_full(schema, None, None)
308
309    def get_iso(self):
310        return self.settings.get_string("iso")
311
312    def set_iso(self, iso):
313        self.settings.set_string("iso", iso)
314
315    def get_aperture(self):
316        return self.settings.get_string("aperture")
317
318    def set_aperture(self, fnum):
319        self.settings.set_string("aperture", fnum)
320
321
322class EclipsePlugin(GObject.Object, Peas.Activatable):
323    '''Handles the plugin activate/deactivation and
324    tracking of camera manager windows. When a window
325    appears, it enables the eclipse functionality on
326    that window'''
327    __gtype_name__ = "EclipsePlugin"
328
329    object = GObject.property(type=GObject.Object)
330
331    def __init__(self):
332        GObject.Object.__init__(self)
333        self.wins = []
334        self.winsigadd = None
335        self.winsigrem = None
336        self.config = None
337        self.script = None
338
339    def do_activate_window(self, win):
340        if not isinstance(win, Entangle.CameraManager):
341            return
342
343        win.add_script(self.script)
344        self.wins.append(win)
345
346    def do_deactivate_window(self, win):
347        if not isinstance(win, Entangle.CameraManager):
348            return
349
350        win.remove_script(self.script)
351        oldwins = self.wins
352        self.wins = []
353        for w in oldwins:
354            if w != win:
355                self.wins.append(w)
356
357    def do_activate(self):
358        if self.config is None:
359            self.config = EclipsePluginConfig(self.plugin_info)
360            self.script = EclipsePluginScript(self.config)
361
362        # Windows can be dynamically added/removed so we
363        # must track this
364        self.winsigadd = self.object.connect(
365            "window-added",
366            lambda app, win: self.do_activate_window(win))
367        self.winsigrem = self.object.connect(
368            "window-removed",
369            lambda app, win: self.do_deactivate_window(win))
370
371        for win in self.object.get_windows():
372            self.do_activate_window(win)
373
374    def do_deactivate(self):
375        self.object.disconnect(self.winsigadd)
376        self.object.disconnect(self.winsigrem)
377        for win in self.object.get_windows():
378            self.do_deactivate_window(win)
379        self.config = None
380        self.script = None
381