application.py 16.7 KB
Newer Older
1
# -*- coding: utf-8 -*-
Jeff Fortin Tam's avatar
Jeff Fortin Tam committed
2
# Pitivi video editor
Thibault Saunier's avatar
Thibault Saunier committed
3 4
# Copyright (c) 2005-2009 Edward Hervey <bilboed@bilboed.com>
# Copyright (c) 2008-2009 Alessandro Decina <alessandro.d@gmail.com>
5
# Copyright (c) 2014 Alexandru Băluț <alexandru.balut@gmail.com>
Edward Hervey's avatar
Edward Hervey committed
6 7 8 9 10 11 12 13 14 15 16 17
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
18
# License along with this program; if not, see <http://www.gnu.org/licenses/>.
19
import os
20
import time
21
from gettext import gettext as _
22

23
from gi.repository import Gio
24
from gi.repository import GLib
25
from gi.repository import GObject
26
from gi.repository import Gst
27
from gi.repository import Gtk
Edward Hervey's avatar
Edward Hervey committed
28

29 30
from pitivi.configure import RELEASES_URL
from pitivi.configure import VERSION
31
from pitivi.effects import EffectsManager
32
from pitivi.mainwindow import MainWindow
33
from pitivi.pluginmanager import PluginManager
34 35 36
from pitivi.project import ProjectManager
from pitivi.settings import GlobalSettings
from pitivi.settings import xdg_cache_home
37 38
from pitivi.shortcuts import ShortcutsManager
from pitivi.shortcuts import show_shortcuts
39
from pitivi.undo.project import ProjectObserver
40 41 42
from pitivi.undo.undo import UndoableActionLog
from pitivi.utils import loggable
from pitivi.utils.loggable import Loggable
43 44
from pitivi.utils.misc import path_from_uri
from pitivi.utils.misc import quote_uri
Thibault Saunier's avatar
Thibault Saunier committed
45
from pitivi.utils.proxy import ProxyManager
46
from pitivi.utils.system import get_system
47
from pitivi.utils.threads import ThreadMaster
48
from pitivi.utils.timeline import Zoomable
Edward Hervey's avatar
Edward Hervey committed
49

50

51
class Pitivi(Gtk.Application, Loggable):
Alexandru Băluț's avatar
Alexandru Băluț committed
52
    """Hello world.
53

54
    Attributes:
55
        action_log (UndoableActionLog): The undo/redo log for the current project.
56
        effects (EffectsManager): The effects which can be applied to a clip.
57
        gui (MainWindow): The main window of the app.
58
        recent_manager (Gtk.RecentManager): Manages recently used projects.
59 60
        project_manager (ProjectManager): The holder of the current project.
        settings (GlobalSettings): The application-wide settings.
61
        system (pitivi.utils.system.System): The system running the app.
62
    """
Edward Hervey's avatar
Edward Hervey committed
63

64
    __gsignals__ = {
65
        "version-info-received": (GObject.SignalFlags.RUN_LAST, None, (object,))
66
    }
Edward Hervey's avatar
Edward Hervey committed
67

68
    def __init__(self):
69
        Gtk.Application.__init__(self,
70
                                 application_id="org.pitivi.Pitivi",
71
                                 flags=Gio.ApplicationFlags.NON_UNIQUE | Gio.ApplicationFlags.HANDLES_OPEN)
72 73
        Loggable.__init__(self)

74 75 76 77 78
        self.settings = None
        self.threads = None
        self.effects = None
        self.system = None
        self.project_manager = ProjectManager(self)
79

80
        self.action_log = None
81
        self.project_observer = None
82
        self._last_action_time = Gst.util_get_timestamp()
83 84

        self.gui = None
85
        self.recent_manager = Gtk.RecentManager.get_default()
86
        self.__inhibit_cookies = {}
87 88 89

        self._version_information = {}

90 91 92
        self._scenario_file = None
        self._first_action = True

93
        Zoomable.app = self
94
        self.shortcuts = ShortcutsManager(self)
95

96
    def write_action(self, action, **kwargs):
97 98 99
        if self._scenario_file is None:
            return

100
        if self._first_action:
101
            self._scenario_file.write(
102
                "description, seek=true, handles-states=true\n")
103 104
            self._first_action = False

105 106 107 108 109
        now = Gst.util_get_timestamp()
        if now - self._last_action_time > 0.05 * Gst.SECOND:
            # We need to make sure that the waiting time was more than 50 ms.
            st = Gst.Structure.new_empty("wait")
            st["duration"] = float((now - self._last_action_time) / Gst.SECOND)
110 111
            self._scenario_file.write(st.to_string())
            self._scenario_file.write("\n")
112 113
            self._last_action_time = now

114 115 116
        if not isinstance(action, Gst.Structure):
            structure = Gst.Structure.new_empty(action)

117 118
            for key, value in kwargs.items():
                key = key.replace("_", "-")
119 120 121 122
                structure[key] = value

            action = structure

123 124
        self._scenario_file.write(action.to_string())
        self._scenario_file.write("\n")
125
        self._scenario_file.flush()
126

127 128 129
    def do_startup(self):
        Gtk.Application.do_startup(self)

130
        # Init logging as early as possible so we can log startup code
Alexandru Băluț's avatar
Alexandru Băluț committed
131
        enable_color = os.environ.get('PITIVI_DEBUG_NO_COLOR', '0') not in ('', '1')
132
        # Let's show a human-readable Pitivi debug output by default, and only
133 134
        # show a crazy unreadable mess when surrounded by gst debug statements.
        enable_crack_output = "GST_DEBUG" in os.environ
135
        loggable.init('PITIVI_DEBUG', enable_color, enable_crack_output)
136 137

        self.info('starting up')
138
        self._setup()
139
        self._check_version()
140 141

    def _setup(self):
142
        # pylint: disable=attribute-defined-outside-init
143
        self.settings = GlobalSettings()
Edward Hervey's avatar
Edward Hervey committed
144
        self.threads = ThreadMaster()
145
        self.effects = EffectsManager()
Thibault Saunier's avatar
Thibault Saunier committed
146
        self.proxy_manager = ProxyManager(self)
147
        self.system = get_system()
148
        self.plugin_manager = PluginManager(self)
149

150 151
        self.project_manager.connect("new-project-loaded", self._new_project_loaded_cb)
        self.project_manager.connect_after("project-closed", self._project_closed_cb)
152
        self.project_manager.connect("project-saved", self.__project_saved_cb)
153

154
        self._create_actions()
155
        self._sync_do_undo()
156

157
    def _create_actions(self):
158
        self.shortcuts.register_group("app", _("General"), position=10)
159 160

        # pylint: disable=attribute-defined-outside-init
161
        self.undo_action = Gio.SimpleAction.new("undo", None)
162
        self.undo_action.connect("activate", self._undo_cb)
163
        self.add_action(self.undo_action)
Ayush Mittal's avatar
Ayush Mittal committed
164
        self.shortcuts.add("app.undo", ["<Primary>z"], self.undo_action,
Jakub Brindza's avatar
Jakub Brindza committed
165
                           _("Undo the most recent action"))
166 167

        self.redo_action = Gio.SimpleAction.new("redo", None)
168
        self.redo_action.connect("activate", self._redo_cb)
169
        self.add_action(self.redo_action)
Ayush Mittal's avatar
Ayush Mittal committed
170
        self.shortcuts.add("app.redo", ["<Primary><Shift>z"], self.redo_action,
171
                           _("Redo the most recent action"))
172 173

        self.quit_action = Gio.SimpleAction.new("quit", None)
174
        self.quit_action.connect("activate", self._quit_cb)
175
        self.add_action(self.quit_action)
Ayush Mittal's avatar
Ayush Mittal committed
176
        self.shortcuts.add("app.quit", ["<Primary>q"], self.quit_action, _("Quit"))
177 178 179 180

        self.show_shortcuts_action = Gio.SimpleAction.new("shortcuts_window", None)
        self.show_shortcuts_action.connect("activate", self._show_shortcuts_cb)
        self.add_action(self.show_shortcuts_action)
Jakub Brindza's avatar
Jakub Brindza committed
181
        self.shortcuts.add("app.shortcuts_window",
182
                           ["<Primary>F1", "<Primary>question"],
Ayush Mittal's avatar
Ayush Mittal committed
183
                           self.show_shortcuts_action,
184
                           _("Show the Shortcuts Window"))
185

186
    def do_activate(self):
187 188 189
        if self.gui:
            # The app is already started and the window already created.
            # Present the already existing window.
190
            if self.system.has_x11():
191 192 193 194 195
                # TODO: Use present() instead of present_with_time() when
                # https://bugzilla.gnome.org/show_bug.cgi?id=688830 is fixed.
                from gi.repository import GdkX11
                x11_server_time = GdkX11.x11_get_server_time(self.gui.get_window())
                self.gui.present_with_time(x11_server_time)
196
            else:
197 198 199
                # On Wayland or Quartz (Mac OS X) backend there is no GdkX11,
                # so just use present() directly here.
                self.gui.present()
200 201
            # No need to show the welcome wizard.
            return
202

203 204
        self.create_main_window()
        self.gui.show_perspective(self.gui.greeter)
205

206
    def create_main_window(self):
207 208
        if self.gui:
            return
209
        self.gui = MainWindow(self)
210
        self.gui.setup_ui()
211 212
        self.add_window(self.gui)

213
    def do_open(self, giofiles, unused_count, unused_hint):
214
        assert giofiles
215
        self.create_main_window()
216
        if len(giofiles) > 1:
217
            self.warning("Opening only one project at a time. Ignoring the rest!")
218
        project_file = giofiles[0]
219
        self.project_manager.load_project(quote_uri(project_file.get_uri()))
220 221
        return True

Edward Hervey's avatar
Edward Hervey committed
222
    def shutdown(self):
Alexandru Băluț's avatar
Alexandru Băluț committed
223
        """Closes the app.
224

Alexandru Băluț's avatar
Alexandru Băluț committed
225 226
        Returns:
            bool: True if successful, False otherwise.
Edward Hervey's avatar
Edward Hervey committed
227
        """
228
        self.debug("shutting down")
229
        # Refuse to close if we are not done with the current project.
230
        if not self.project_manager.close_running_project():
231 232
            self.warning(
                "Not closing since running project doesn't want to close")
Edward Hervey's avatar
Edward Hervey committed
233
            return False
234 235
        if self.gui:
            self.gui.destroy()
Alexandru Băluț's avatar
Alexandru Băluț committed
236
        self.threads.wait_all_threads()
237
        self.settings.store_settings()
238
        self.quit()
Edward Hervey's avatar
Edward Hervey committed
239 240
        return True

241
    def _set_scenario_file(self, uri):
242 243 244 245 246
        if uri:
            project_path = path_from_uri(uri)
        else:
            # New project.
            project_path = None
247
        if 'PITIVI_SCENARIO_FILE' in os.environ:
248
            scenario_path = os.environ['PITIVI_SCENARIO_FILE']
249
        else:
250
            cache_dir = xdg_cache_home("scenarios")
251
            scenario_name = str(time.strftime("%Y%m%d-%H%M%S"))
252
            if project_path:
253
                scenario_name += os.path.splitext(project_path.replace(os.sep, "_"))[0]
254
            scenario_path = os.path.join(cache_dir, scenario_name + ".scenario")
255

256 257
        scenario_path = path_from_uri(quote_uri(scenario_path))
        self._scenario_file = open(scenario_path, "w")
258

259 260 261 262
        if project_path and not project_path.endswith(".scenario"):
            # It's an xges file probably.
            with open(project_path) as project:
                content = project.read().replace("\n", "")
263
                self.write_action("load-project",
264
                                  serialized_content=content)
265

266
    def _new_project_loaded_cb(self, unused_project_manager, project):
267 268 269 270 271
        uri = project.get_uri()
        if uri:
            # We remove the project from recent projects list
            # and then re-add it to this list to make sure it
            # gets positioned at the top of the recent projects list.
272 273 274 275 276
            try:
                self.recent_manager.remove_item(uri)
            except GLib.Error as e:
                if e.domain != "gtk-recent-manager-error-quark":
                    raise e
277
            self.recent_manager.add_item(uri)
278

279 280
        self.action_log = UndoableActionLog()
        self.action_log.connect("pre-push", self._action_log_pre_push_cb)
281
        self.action_log.connect("commit", self._action_log_commit)
282
        self.action_log.connect("move", self._action_log_move_cb)
283
        self.project_observer = ProjectObserver(project, self.action_log)
284

285
        self._set_scenario_file(project.get_uri())
286

287 288 289 290
    def __project_saved_cb(self, unused_project_manager, unused_project, uri):
        if uri:
            self.recent_manager.add_item(uri)

291
    def _project_closed_cb(self, unused_project_manager, project):
292
        if project.loaded:
293
            self.action_log = None
294
            self._sync_do_undo()
295

296 297 298 299 300
        if self._scenario_file:
            self.write_action("stop")
            self._scenario_file.close()
            self._scenario_file = None

301
    def _check_version(self):
Alexandru Băluț's avatar
Alexandru Băluț committed
302
        """Checks online for new versions of the app."""
303
        self.info("Requesting version information async")
304
        giofile = Gio.File.new_for_uri(RELEASES_URL)
305
        giofile.load_contents_async(None, self._version_info_received_cb, None)
Paul Lange's avatar
Paul Lange committed
306

307
    def _version_info_received_cb(self, giofile, result, user_data):
Alexandru Băluț's avatar
Alexandru Băluț committed
308
        # pylint: disable=broad-except
Paul Lange's avatar
Paul Lange committed
309
        try:
310
            raw = giofile.load_contents_finish(result)[1]
311 312 313
            if not isinstance(raw, str):
                raw = raw.decode()
            raw = raw.split("\n")
314
            # Split line at '=' if the line is not empty or a comment line
Paul Lange's avatar
Paul Lange committed
315 316 317 318 319
            data = [element.split("=") for element in raw
                    if element and not element.startswith("#")]

            # search newest version and status
            status = "UNSUPPORTED"
320
            current_version = None
Paul Lange's avatar
Paul Lange committed
321
            for version, version_status in data:
322
                if VERSION == version:
Paul Lange's avatar
Paul Lange committed
323 324
                    status = version_status
                if version_status.upper() == "CURRENT":
325
                    # This is the latest.
Paul Lange's avatar
Paul Lange committed
326
                    current_version = version
327
                    self.info("Latest software version is %s", current_version)
Paul Lange's avatar
Paul Lange committed
328

329
            version_split = [int(i) for i in VERSION.split(".")]
330 331
            current_version_split = [int(i)
                                     for i in current_version.split(".")]
332
            if version_split > current_version_split:
333
                status = "CURRENT"
334 335
                self.info(
                    "Running version %s, which is newer than the latest known version. Considering it as the latest current version.", VERSION)
Alexandru Băluț's avatar
Alexandru Băluț committed
336
            elif status == "UNSUPPORTED":
337 338
                self.warning(
                    "Using an outdated version of Pitivi (%s)", VERSION)
339

340 341 342
            self._version_information["current"] = current_version
            self._version_information["status"] = status
            self.emit("version-info-received", self._version_information)
Lubosz Sarnecki's avatar
Lubosz Sarnecki committed
343
        except Exception as e:
344 345
            self.warning("Version info could not be read: %s", e)

346
    def is_latest(self):
Alexandru Băluț's avatar
Alexandru Băluț committed
347
        """Whether the app's version is the latest as far as we know."""
348 349 350
        status = self._version_information.get("status")
        return status is None or status.upper() == "CURRENT"

351
    def get_latest(self):
Alexandru Băluț's avatar
Alexandru Băluț committed
352
        """Get the latest version of the app or None."""
353
        return self._version_information.get("current")
354

355
    def _quit_cb(self, unused_action, unused_param):
356 357
        self.shutdown()

358
    def _undo_cb(self, unused_action, unused_param):
359 360
        self.action_log.undo()

361
    def _redo_cb(self, unused_action, unused_param):
362 363
        self.action_log.redo()

364 365 366
    def _show_shortcuts_cb(self, unused_action, unused_param):
        show_shortcuts(self)

367
    def _action_log_pre_push_cb(self, unused_action_log, action):
368
        scenario_action = action.as_scenario_action()
369 370
        if scenario_action:
            self.write_action(scenario_action)
371

372
    def _action_log_commit(self, action_log, unused_stack):
373
        if action_log.is_in_transaction():
374
            return
375
        self._sync_do_undo()
376

377
    def _action_log_move_cb(self, action_log, unused_stack):
378
        self._sync_do_undo()
379

380
    def _sync_do_undo(self):
381 382
        can_undo = self.action_log and bool(self.action_log.undo_stacks)
        self.undo_action.set_enabled(bool(can_undo))
383

384 385
        can_redo = self.action_log and bool(self.action_log.redo_stacks)
        self.redo_action.set_enabled(bool(can_redo))
386

387 388 389
        if not self.project_manager.current_project:
            return

390
        dirty = self.action_log and self.action_log.dirty()
391
        self.project_manager.current_project.set_modification_state(dirty)
392 393
        # In the tests we do not want to create any gui
        if self.gui is not None:
394
            self.gui.editor.show_project_status()
395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427

    def simple_inhibit(self, reason, flags):
        """Informs the session manager about actions to be inhibited.

        Keeps track of the reasons received. A specific reason should always
        be accompanied by the same flags. Calling the method a second time
        with the same reason has no effect unless `simple_uninhibit` has been
        called in the meanwhile.

        Args:
            reason (str): The reason for which to perform the inhibition.
            flags (Gtk.ApplicationInhibitFlags): What should be inhibited.
        """
        if reason in self.__inhibit_cookies:
            self.debug("Inhibit reason already active: %s", reason)
            return
        self.debug("Inhibiting %s for %s", flags, reason)
        cookie = self.inhibit(self.gui, flags, reason)
        self.__inhibit_cookies[reason] = cookie

    def simple_uninhibit(self, reason):
        """Informs the session manager that an inhibition is not needed anymore.

        Args:
            reason (str): The reason which is not valid anymore.
        """
        try:
            cookie = self.__inhibit_cookies.pop(reason)
        except KeyError:
            self.debug("Inhibit reason not active: %s", reason)
            return
        self.debug("Uninhibiting %s", reason)
        self.uninhibit(cookie)