application.py 16.7 KB
Newer Older
1
# -*- coding: utf-8 -*-
2
# Pitivi video editor
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 18
#
# 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
# License along with this program; if not, write to the
Hicham HAOUARI's avatar
Hicham HAOUARI committed
19 20
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
# Boston, MA 02110-1301, USA.
21
import os
22
import time
23
from gettext import gettext as _
24

25
from gi.repository import Gio
26
from gi.repository import GLib
27
from gi.repository import GObject
28
from gi.repository import Gst
29
from gi.repository import Gtk
30

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

53

54
class Pitivi(Gtk.Application, Loggable):
55
    """Hello world.
56

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

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

71
    def __init__(self):
72
        Gtk.Application.__init__(self,
73
                                 application_id="org.pitivi.Pitivi",
74
                                 flags=Gio.ApplicationFlags.NON_UNIQUE |
75
                                 Gio.ApplicationFlags.HANDLES_OPEN)
76 77
        Loggable.__init__(self)

78 79 80 81 82
        self.settings = None
        self.threads = None
        self.effects = None
        self.system = None
        self.project_manager = ProjectManager(self)
83

84
        self.action_log = None
85
        self.project_observer = None
86
        self._last_action_time = Gst.util_get_timestamp()
87 88

        self.gui = None
89
        self.recent_manager = Gtk.RecentManager.get_default()
90
        self.__inhibit_cookies = {}
91 92 93

        self._version_information = {}

94 95 96
        self._scenario_file = None
        self._first_action = True

97
        Zoomable.app = self
98
        self.shortcuts = ShortcutsManager(self)
99

100
    def write_action(self, action, **kwargs):
101 102 103
        if self._scenario_file is None:
            return

104
        if self._first_action:
105
            self._scenario_file.write(
106
                "description, seek=true, handles-states=true\n")
107 108
            self._first_action = False

109 110 111 112 113
        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)
114 115
            self._scenario_file.write(st.to_string())
            self._scenario_file.write("\n")
116 117
            self._last_action_time = now

118 119 120
        if not isinstance(action, Gst.Structure):
            structure = Gst.Structure.new_empty(action)

121 122
            for key, value in kwargs.items():
                key = key.replace("_", "-")
123 124 125 126
                structure[key] = value

            action = structure

127 128
        self._scenario_file.write(action.to_string())
        self._scenario_file.write("\n")
129
        self._scenario_file.flush()
130

131 132 133
    def do_startup(self):
        Gtk.Application.do_startup(self)

134
        # Init logging as early as possible so we can log startup code
135 136
        enable_color = not os.environ.get(
            'PITIVI_DEBUG_NO_COLOR', '0') in ('', '1')
137
        # Let's show a human-readable Pitivi debug output by default, and only
138 139
        # show a crazy unreadable mess when surrounded by gst debug statements.
        enable_crack_output = "GST_DEBUG" in os.environ
140
        loggable.init('PITIVI_DEBUG', enable_color, enable_crack_output)
141 142

        self.info('starting up')
143 144 145 146
        self._setup()
        self._checkVersion()

    def _setup(self):
147
        self.settings = GlobalSettings()
Edward Hervey's avatar
Edward Hervey committed
148
        self.threads = ThreadMaster()
149
        self.effects = EffectsManager()
150
        self.proxy_manager = ProxyManager(self)
151
        self.system = get_system()
152
        self.plugin_manager = PluginManager(self)
153

154 155
        self.project_manager.connect("new-project-loaded", self._new_project_loaded_cb)
        self.project_manager.connect_after("project-closed", self._project_closed_cb)
156
        self.project_manager.connect("project-saved", self.__project_saved_cb)
157

158
        self._createActions()
159 160
        self._syncDoUndo()

161
    def _createActions(self):
162
        self.shortcuts.register_group("app", _("General"), position=10)
163 164 165
        self.undo_action = Gio.SimpleAction.new("undo", None)
        self.undo_action.connect("activate", self._undoCb)
        self.add_action(self.undo_action)
166
        self.shortcuts.add("app.undo", ["<Primary>z"],
167
                           _("Undo the most recent action"))
168 169 170 171

        self.redo_action = Gio.SimpleAction.new("redo", None)
        self.redo_action.connect("activate", self._redoCb)
        self.add_action(self.redo_action)
172
        self.shortcuts.add("app.redo", ["<Primary><Shift>z"],
173
                           _("Redo the most recent action"))
174 175 176 177

        self.quit_action = Gio.SimpleAction.new("quit", None)
        self.quit_action.connect("activate", self._quitCb)
        self.add_action(self.quit_action)
178
        self.shortcuts.add("app.quit", ["<Primary>q"], _("Quit"))
179 180 181 182

        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)
183
        self.shortcuts.add("app.shortcuts_window",
184
                           ["<Primary>F1", "<Primary>question"],
185
                           _("Show the Shortcuts Window"))
186

187
    def do_activate(self):
188 189 190
        if self.gui:
            # The app is already started and the window already created.
            # Present the already existing window.
191
            if self.system.has_x11():
192 193 194 195 196
                # 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)
197
            else:
198 199 200
                # On Wayland or Quartz (Mac OS X) backend there is no GdkX11,
                # so just use present() directly here.
                self.gui.present()
201 202
            # No need to show the welcome wizard.
            return
203

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

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

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

Edward Hervey's avatar
Edward Hervey committed
223
    def shutdown(self):
224
        """Closes the app.
225

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

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

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

260 261 262 263
        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", "")
264
                self.write_action("load-project",
265
                                  serialized_content=content)
266

267
    def _new_project_loaded_cb(self, unused_project_manager, project):
268 269 270 271 272
        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.
273 274 275 276 277 278
            try:
                self.recent_manager.remove_item(uri)
            except GLib.Error as e:
                if e.domain != "gtk-recent-manager-error-quark":
                    raise e
                pass
279
            self.recent_manager.add_item(uri)
280

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

287 288
        self._setScenarioFile(project.get_uri())

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

293
    def _project_closed_cb(self, unused_project_manager, project):
294
        if project.loaded:
295 296
            self.action_log = None
            self._syncDoUndo()
297

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

Paul Lange's avatar
Paul Lange committed
303
    def _checkVersion(self):
304
        """Checks online for new versions of the app."""
305
        self.info("Requesting version information async")
306
        giofile = Gio.File.new_for_uri(RELEASES_URL)
307
        giofile.load_contents_async(None, self._version_info_received_cb, None)
Paul Lange's avatar
Paul Lange committed
308

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

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

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

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

    def isLatest(self):
348
        """Whether the app's version is the latest as far as we know."""
349 350 351 352
        status = self._version_information.get("status")
        return status is None or status.upper() == "CURRENT"

    def getLatest(self):
353
        """Get the latest version of the app or None."""
354
        return self._version_information.get("current")
355 356 357 358 359 360 361 362 363 364

    def _quitCb(self, unused_action, unused_param):
        self.shutdown()

    def _undoCb(self, unused_action, unused_param):
        self.action_log.undo()

    def _redoCb(self, unused_action, unused_param):
        self.action_log.redo()

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

368 369 370 371 372 373 374 375 376
    def _action_log_pre_push_cb(self, unused_action_log, action):
        try:
            st = action.asScenarioAction()
        except NotImplementedError:
            self.warning("No serialization method for action %s", action)
            return
        if st:
            self.write_action(st)

377 378
    def _actionLogCommit(self, action_log, unused_stack):
        if action_log.is_in_transaction():
379
            return
380
        self._syncDoUndo()
381

382
    def _action_log_move_cb(self, action_log, unused_stack):
383
        self._syncDoUndo()
384

385 386 387
    def _syncDoUndo(self):
        can_undo = self.action_log and bool(self.action_log.undo_stacks)
        self.undo_action.set_enabled(bool(can_undo))
388

389 390
        can_redo = self.action_log and bool(self.action_log.redo_stacks)
        self.redo_action.set_enabled(bool(can_redo))
391

392 393 394
        if not self.project_manager.current_project:
            return

395
        dirty = self.action_log and self.action_log.dirty()
396
        self.project_manager.current_project.setModificationState(dirty)
397 398
        # In the tests we do not want to create any gui
        if self.gui is not None:
399
            self.gui.editor.showProjectStatus()
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 428 429 430 431 432

    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)