TweetFollow Us on Twitter

Horse Feathers

Volume Number: 20 (2004)
Issue Number: 7
Column Tag: Programming

QuickTime Toolkit

by Tim Monroe

Horse Feathers

Introduction

In the previous article ("Tickle Me" in MacTech, June 2004), we took a look at using the Tcl scripting language and the Tk graphical user interface toolkit to build a QuickTime application. We saw how to use the QuickTimeTcl extension to add to a basic Tcl/Tk application the ability to open and display QuickTime movies. Figure 1 shows a movie displayed by our sample application, called TickLeez.


Figure 1: A movie window displayed by TickLeez (Macintosh)

In this article, I want to continue investigating QuickTimeTcl. In particular, we'll see how to configure our application to react to keyboard and operating system events. We'll also see how to implement movie editing. By the end of this article, TickLeez will incorporate all the standard movie playback and editing behaviors.

Event Bindings

Last time, we saw how to create the menu bar and menus for the TickLeez application. Later in this article, we'll learn how to adjust the state of menu items according to the current edited state of the frontmost movie window. Right now, we need to create some Tk bindings for a handful of events.

An event binding attaches a Tcl command to a specific event related to a movie window or to some other event, such as a keyboard event. The -accelerator options we specified when creating the menu items affect only the appearance of the menu items. To enable the keyboard shortcuts, we need to call the bind command, for instance like this:

bind .$winName <$appData(modKey)-n> {newDoc}

Tk also allows us to create bindings for activate and deactivate events for a window and for mouse-enter and mouse-exit events. Our complete set of event bindings is established in the setupBindings procedure, shown in Listing 1.

Listing 1: Setting up key bindings

setupBindings
proc setupBindings {winName} {
   global appData
   bind .$winName <$appData(modKey)-n> {newDoc}
   bind .$winName <$appData(modKey)-o> {openDoc}
   bind .$winName <$appData(modKey)-q> {exit}
      
   if {[isMovieWindow $winName]} {
      # window state-change bindings
      bind .$winName <Activate> {adjustMenus}
      bind .$winName <Deactivate> {adjustMenus}
      
      # accelerator key bindings
      bind .$winName <$appData(modKey)-w> {closeDoc}
      bind .$winName <$appData(modKey)-s> {saveDoc}
      bind .$winName <$appData(modKey)-S> {saveAsDoc}
      
      bind .$winName <$appData(modKey)-z> {undoDoc}
      bind .$winName <$appData(modKey)-x> {editDoc cut}
      bind .$winName <$appData(modKey)-c> {editDoc copy}
      bind .$winName <$appData(modKey)-v> {editDoc paste}
      bind .$winName <$appData(modKey)-a> {selectDoc all}
      bind .$winName <$appData(modKey)-b> {selectDoc none}
      
      bind .$winName <$appData(modKey)-KeyPress-1> \
                                                {puts "TO BE PROVIDED"}
      
      # miscellaneous bindings
      bind .$winName <Delete> {editDoc cut}
      bind .$winName <KeyPress> "handleKey $winName %K"
      bind .$winName <Leave> ".$winName configure \
                                                -cursor arrow"
   }
}

Note the Leave binding: it's mainly intended for QuickTime VR movies, where the cursor is not automatically changed to the normal arrow cursor when it moves outside of the movie rectangle. (Other media types that alter the cursor, such as Flash, do not have this problem, so one could reasonably consider the VR behavior to be a bug.) Keep in mind that this workaround is not perfect: the cursor does not change to an arrow until the cursor hotspot has completely exited the movie window. This means that it will remain as the current VR cursor if moved into the window's title bar. Addressing that issue is left as an exercise for the reader.

Movie Playback

Once we've loaded a movie into a movie window (using the configure command), QuickTimeTcl takes care of all the nitty-gritty details of passing user or system events to the movie and making sure it reacts appropriately to those events. Mouse events on the movie controller bar work fine. Moreover, QuickTimeTcl takes care of tasking the movie often enough to ensure uninterrupted playback of linear movies or smooth user interaction with nonlinear movies like QuickTime VR movies or wired sprite movies.

Handling Keyboard Events

QuickTimeTcl, however, does not appear to provide built-in support for the standard methods of controlling movie playback using the keyboard. With a linear movie, for instance, pressing the space bar should toggle the current play state of the movie. And with a QuickTime VR movie, pressing the left arrow key should pan the movie to the left.

As you've probably guessed, we can support these behaviors by creating the appropriate event bindings. The setupBindings function considered just above binds key presses to the handleKey function, like so:

bind .$winName <KeyPress> "handleKey $winName %K"

The "%K" keyword is replaced with the keysym, which is a special Tk designator for a key on the keyboard. For instance, the keysym for the space bar is "space", and the keysym for the Shift key is "Shift_L".

Our implementation of handleKey, shown in Listing 2, first determines whether the specified movie window is a QuickTime VR movie or not. (The command ispanoramic is slightly misnamed, as it also returns 1 for a QuickTime VR object movie.) For a QuickTime VR movie, we use the movie widget commands pan, tilt, and fieldofview to adjust the view parameters. For linear movies, we use the commands step, play, and stop to adjust the movie time and play rate. We also adjust the movie's volume when we receive an up arrow or down arrow key.

Listing 2: Handling key presses

handleKey
proc handleKey {winName key} {
   global appData
    
   set movie $appData($winName,movie)
   if {[$movie ispanoramic]} {
      # handle QTVR movie controller key bindings 
      # (and, yes, "ispanoramic" is true for object movies too)
      switch $key {
         "Right" {$movie pan [expr [$movie pan] - 10]}
         "Left" {$movie pan [expr [$movie pan] + 10]}
         "Up" {$movie tilt [expr [$movie tilt] + 10]}
         "Down" {$movie tilt [expr [$movie tilt] - 10]}
         "Shift_L" {$movie fieldofview [expr [$movie \
                              fieldofview] - 5]}
         "Control_L" {$movie fieldofview [expr [$movie \
                              fieldofview] + 5]}
      }
   } else {
      # handle standard movie controller key bindings
      switch $key {
         "Right" {$movie step 1}
         "Left" {$movie step -1}
         "Up" {$movie configure -volume [min 255 \
                  [expr 64 + [$movie cget -volume]]]}
         "Down" {$movie configure -volume [max 0 \
                  [expr -64 + [$movie cget -volume]]]}
         "space" {if {[$movie rate] == 0} {$movie play} 
                                                else {$movie stop}}
      }
   }
}

Installing a Movie Controller Callback Function

At this point, TickLeez can open and display QuickTime movies, and it can handle all the standard means of user interaction with the movie. Before we move on to consider how to support movie editing, I should mention that QuickTimeTcl provides a way to observe and possibly override actions associated with the movie; that is to say, it provides a wrapper for QuickTime's movie controller action filter function.

We installed this movie controller callback function when we configured our movie widget, by passing a procedure name to the -mccommand argument:

.$winName.mov configure -mccommand controllerProc \
               -mcedit 1 -resizable 1

The specified procedure is passed the name of the movie widget, a string representing the action, and a possibly empty list of parameters associated with that action. For linear movies, these actions include activate, customButtonClick, deactivate, key, goToTime, mouseDown, play, setSelectionBegin, setSelectionDuration, and setVolume. For QuickTime VR movies, these actions also include pan, tilt, fieldofview, and triggerhotspot. Listing 3 shows the basic form of a movie controller callback function.

Listing 3: Handling movie controller actions

controllerProc
proc controllerProc {w what {par {}}} {
   if {![isMovieWindow [string range $w 1 end]]} {
      return
   }
   if {$what == "key"} {
      puts "got key event in controller action proc"
   }
   
}

Normally the action is processed by the movie controller after our callback procedure returns. To suppress an action, we can execute this line of code:

return -code 3

Movie Editing

QuickTimeTcl provides a number of movie widget commands for performing the standard movie editing operations (cut, copy, paste, and so forth). It also provides support for multilevel undo, that is, the ability to undo more than one previous editing operation. None of the QuickTime-savvy applications that we've built hitherto supports this very nice feature, except for the MooVeez application we built in an earlier article, where the multilevel undo is provided by the Cocoa application framework. (See "The Cocoanuts" in MacTech, December 2002.) In this section, we'll see how to implement editing in TickLeez and how to adjust the File and Edit menus in accordance with the edited state of the frontmost movie window.

Handling Editing Operations

We've already seen how to invoke an edit command when the appropriate menu item is selected. For instance, in the setupMenus procedure (Listing 4 in the previous article), we saw this line of code for setting up the Clear menu item:

$m add command -label "Clear" -command {editDoc clear}

So, when the user selects the Clear menu item, the editDoc procedure is called with the "clear" parameter. Listing 4 shows our implementation of the editDoc procedure.

Listing 4: Editing a movie

editDoc
proc editDoc {op} {
   global appData
   set winName [topMovieWindow]
   $appData($winName,movie) $op
   if {$op != "copy"} {
      set n [incr appData($winName,undoLevel)]
      set appData($winName,undoOp,$n) $op
      setWindowDirty $winName 1 
   }
   adjustSize $winName
   adjustMenus
}

The key step here is the line of code that sends the specified operation string $op to the frontmost movie widget as a command. The movie widget supports these editing commands: cut, copy, paste, clear, add, and undo. Notice that we also increment the widget's undo level and store the operation string itself in the movie window's global data array. In a moment, we'll see how these items are used to support multilevel undo.

The editDoc function also calls the setWindowDirty function (shown in Listing 5) to mark the movie window as having been changed. (Note that QuickTimeTcl provides a built-in procedure haschanged that we could use instead of reading this stored value; however, I prefer to keep track of changes to the movie myself.)

Listing 5: Marking a movie window as changed

setWindowDirty
proc setWindowDirty {winName state} {
   global appData
   set appData($winName,dirty) $state
   if {[string match "mac*" $appData(os)]} {
      wm attributes .$winName -modified $state
   }
}

Notice that, on Macintosh computers, we also set the modified attribute of the movie window; on Mac OS X, this has the effect of drawing a dot inside the window's close button, as shown in Figure 2.


Figure 2: A modified movie window

Undoing Editing Operations

Whenever an editing operation is performed, QuickTimeTcl adds information about the state of the movie just prior to the edit to an edit stack. The number of the topmost item on the stack is called the undo level. When a movie is first opened, its undo level is 0. Subsequent editing operations increment the undo level.

QuickTimeTcl allows us to undo one or more edits by executing the undo command. This command requires one parameter, which specifies the undo level we want to revert to. In TickLeez, we shall always revert to the previous edit, so we'll decrement the undo level by one and pass that number to undo. This is how we implement multilevel undo. Listing 6 shows our definition of the undoDoc function.

Listing 6: Undoing an edit

undoDoc
proc undoDoc {} {
   global appData
   set winName [topMovieWindow]
   if {$appData($winName,undoLevel) > 0} {
      incr appData($winName,undoLevel) -1
      $appData($winName,movie) undo \
                                       $appData($winName,undoLevel)
   }
   
   if {$appData($winName,undoLevel) == 0} {
      setWindowDirty $winName 0 
   }
   
   adjustMenus
}

As you can see, if the user undoes all edits (so that the undo level reaches 0 once again), we call setWindowDirty with the parameter 0 to mark the window as not dirty.

Selecting All or None of a Movie

The Edit menu in TickLeez contains the Select All and Select None menu items. In previous articles, we've seen how to handle those items quite easily. For instance, we can handle the Select All item by setting the beginning of the movie selection to movie time 0 and the duration of the movie selection to the duration of the entire movie.

QuickTimeTcl provides the select command that we can use to set the movie selection to a specified range, but it does not provide a command that returns the duration of a movie directly. Instead, QuickTimeTcl provides the gettime command, which returns an array of four movie-related times: the current time, the length of the movie, the movie time scale, and the movie poster time. For a specific movie window, we can retrieve that array like this:

array set timeArr [$appData($winName,movie) gettime]

Once we've successfully done that, we can read the movie duration by looking at the $timeArr(-movieduration) element in the array; and we can read the current movie time by looking at the $timeArr(-movietime) element. Then we can handle the two selection menu items using the selectDoc function, defined in Listing 7.

Listing 7: Selecting all or none of a movie

selectDoc
proc selectDoc {op} {
   global appData
   set winName [topMovieWindow]
   
   array set timeArr [$appData($winName,movie) gettime]
   switch -- ${op} {
       all {$appData($winName,movie) select \
               0 $timeArr(-movieduration)}
       none {$appData($winName,movie) select \
               $timeArr(-movietime) 0}
   }
}

Enabling and Disabling Menu Items

As you know, we need to adjust some of the items in the File and Edit menus based on the current state of the frontmost movie window (if one exists). For instance, if a movie has been edited, we need to enable the Save menu item in the File menu so that the user can save those changes, if desired. Listing 8 shows our implementation of the adjustMenus function, which is called by many of the editing functions we've encountered recently. We handle the Save menu item by looking at the dirty field of the stored movie window data, like this:

if {$appData($winName,dirty) == 1} {
   $bar.file entryconfigure "Save" -state active}

Here, we use Tk's entryconfigure command to set the state of a menu item.

We can determine whether other menu items should be enabled or disabled by executing the controllerinfo command. Like the gettime command we just considered, controllerinfo fills an array with a set of entries, in this case with entries that indicate the current state of the movie controller.

array set infoArr [$appData($winName,movie) controllerinfo]

For instance, if the $infoArr(-cutavailable) item is 1, then we want to enable the Cut menu item.

Listing 8: Adjusting the menus and menu items

adjustMenus
proc adjustMenus {} {
   global appData
   
   set winName [topMovieWindow]
   set bar .${winName}mbar
   if {[string equal $appData(os) "windows"]} {
      if {[string equal $winName ""]} {
         return
      }
   }
   # set the default state
   $bar.file entryconfigure "New" -state active
   $bar.file entryconfigure "Open..." -state active
   $bar.file entryconfigure "Close" -state disabled
   $bar.file entryconfigure "Save" -state disabled
   $bar.file entryconfigure "Save As..." -state disabled
   
   $bar.edit entryconfigure "Undo" -state disabled
   $bar.edit entryconfigure "Cut" -state disabled
   $bar.edit entryconfigure "Copy" -state disabled
   $bar.edit entryconfigure "Paste" -state disabled
   $bar.edit entryconfigure "Clear" -state disabled
   $bar.edit entryconfigure "Select All" -state disabled
   $bar.edit entryconfigure "Select None" -state disabled
   
   $bar.movie entryconfigure "Hide Controller Bar" \
            -state disabled
   if {[string equal $appData(os) "windows"]} {
      $bar.help entryconfigure "About TickLeez" -state active
   } else {
      $bar.apple entryconfigure "About TickLeez" \
            -state active
   }
   
   if {[isMovieWindow $winName]} {
      $bar.file entryconfigure "Close" -state active
      $bar.file entryconfigure "Save As..." -state active
      $bar.edit entryconfigure "Select All" -state active
      $bar.edit entryconfigure "Select None" -state active
   
      if {$appData($winName,dirty) == 1} {
         $bar.file entryconfigure "Save" -state active}
      
      array set infoArr [$appData($winName,movie) \
                                                         controllerinfo]
      if {$appData($winName,undoLevel) > 0} {
         $bar.edit entryconfigure "Undo" -state active}
      if {$infoArr(-cutavailable) == 1} {
         $bar.edit entryconfigure "Cut" -state active}
      if {$infoArr(-copyavailable) == 1} {
         $bar.edit entryconfigure "Copy" -state active}
      if {$infoArr(-pasteavailable) == 1} {
         $bar.edit entryconfigure "Paste" -state active}
      if {$infoArr(-clearavailable) == 1} {
         $bar.edit entryconfigure "Clear" -state active}
      if {$appData($winName,undoLevel) > 0} {
         set op \ $appData($winName,undoOp,$appData($winName,undoLevel))
         $bar.edit entryconfigure "Undo*" -label \
                                    "Undo [string totitle $op]"
      }
      
      $bar.movie entryconfigure "Hide Controller Bar" \
            -state active
   }
}

File Manipulation

Our final task before wrapping up TickLeez for delivery to end users is to handle the document-related operations -- that is saving an edited movie, saving a movie into a new file, closing a movie window, and ultimately quitting the application itself. As we've seen in previous articles, we need to track the state of a movie and prompt the user to save or discard changes to it when the movie window is to be closed or the application is to be quit.

Closing a Movie Window

When the user selects the Close menu item in the File menu (or types the appropriate keyboard shortcut), TickLeez executes the closeDoc procedure, which is defined in Listing 9.

Listing 9: Handling the Close menu item

closeDoc
proc closeDoc {} {
   attemptClose [topMovieWindow] closing
}

As you can see, closeDoc finds the name of the frontmost movie window and passes that name as an argument to the attemptClose function, along with the action name "closing". The attemptClose function needs to look at the dirty state of the window and, if necessary, display a dialog box asking the user to save or discard the changes to the movie. Listing 10 shows our definition of attemptClose.

Listing 10: Prompting the user to save a changed movie

attemptClose
proc attemptClose {winName action} {
   global appData
   set closeWindow 1
   if {$appData($winName,dirty) == 1} {
      set movieName [file tail $appData($winName,fileName)]
   
      set answer [tk_messageBox \
         -parent .$winName \
         -title "Save changes before $action" \
         -message "Do you want to save the changes you made \
                     to the document \u201C$movieName\u201D \
                     before $action?" \
         -type yesnocancel \
         -icon warning]
   
      switch $answer {
         yes         {set closeWindow [saveDoc]}
         cancel   {set closeWindow 0}
         no          {set closeWindow 1}
      }
   }
   if {$closeWindow == 1} {disposeDoc $winName}
   return $closeWindow
}

We display the Save Changes dialog box by calling the tk_messageBox function, this time with the yesnocancel type. Figure 3 shows the Macintosh dialog box, and Figure 4 shows the Windows dialog box.


Figure 3: The Save Changes dialog box of TickLeez (Macintosh)


Figure 4: The Save Changes dialog box of TickLeez (Windows)

Notice in Listing 10 that if the user selects Yes, we call the saveDoc procedure (defined later) and then set the closeWindow variable to the value it returns. We set the closeWindow variable to 1 if the user selects No and to 0 if the user selects Cancel. If closeWindow is 1, we call the disposeDoc function, defined in Listing 11.

Listing 11: Disposing a movie window and its associated data

disposeDoc
proc disposeDoc {winName} {
   global appData
   
   if {![isMovieWindow $winName]} {return}
   destroy .$winName
   array unset appData $winName,*
   adjustMenus
}

The key step in disposeDoc is to call the Tk function destroy, which removes the specified window from the screen and dispose of any memory it occupies. We also call the array unset command to remove from the appData associative array any elements associated with the movie window.

Saving a Changed Movie

As we've seen, TickLeez calls the saveDoc function when the user selects the Save menu item (or types the appropriate keyboard equivalent). In addition, the attemptClose method (Listing 10) calls the saveDoc function if the user elects to save the changes to a window that is about to close. It's easy to implement saveDoc, using the save command provided by QuickTimeTcl, as shown in Listing 12.

Listing 12: Saving a movie

saveDoc
proc saveDoc {} {
   global appData
   set winName [topMovieWindow]
   # if the movie file is in our temporary directory, alert the user
   if {[string compare \
      -length [string length $appData(tempDir)] \
      $appData($winName,fileName) $appData(tempDir)] == 0} {
      tk_messageBox \
         -parent .$winName \
         -title "Warning" \
         -message "Cannot save this movie into the current \
            folder.\nPlease choose \u201CSave As...\u201D to \
            select a different folder." \
         -type ok \
         -icon warning
      return    0
   }
   
   $appData($winName,movie) save
   
   setWindowDirty $winName 0 
   adjustMenus
   return 1
}

Recall that QuickTimeTcl requires a filename for each open movie and that we create a temporary file when the user selects the New command in the File menu. If the file we are saving is located in the designated temporary directory, we want to display the alert sheet shown in Figure 5 to force the user to save the file elsewhere.


Figure 5: The bad directory alert sheet

Saving a Movie into a New File

When the user selects the Save As menu item in the File menu, we need to elicit a location for the new movie file and then save the movie into that file. In TickLeez, we'll execute this line of code:

set newFile [tk_getSaveFile]

The tk_getSaveFile function displays a dialog box like the one shown in Figure 6. It returns as its result the full pathname of the specified file, or an empty string if the user did not specify a file.


Figure 6: The file-saving dialog box displayed by Tk

If the user does specify a file, we can write the movie into that file by calling the QuickTimeTcl command flatten, as shown in Listing 13

Listing 13: Saving a movie into a new file

saveAsDoc
proc saveAsDoc {} {
   global appData
   set winName [topMovieWindow]
   set newFile [tk_getSaveFile]
   if {$newFile != ""} {
      $appData($winName,movie) flatten $newFile
      $appData($winName,movie) configure -file $newFile
      
      wm title .$winName [file tail $newFile]
      
      set appData($winName,dirty) 0   
      set appData($winName,fileName) $newFile   
      set appData($winName,undoLevel) 0
      
      setWindowDirty $winName 0 
   }
   
   adjustMenus
}

As you know, the standard behavior of a Save As operation is for the new movie to replace the existing movie in the existing movie window. Accordingly, saveAsDoc calls the QuickTimeTcl command configure with the full pathname of the new movie file. Then it resets the window title and several fields of the global data array associated with the movie.

Quitting the Application

When the user decides to quit TickLeez, we need to perform the standard quitting-time operations (such as making sure the user saves or discards any unsaved changes to the movie windows). When the user selects the Quit menu item, Tcl calls its own exit function, which performs any Tcl-specific cleanup tasks. We can make sure that our own cleanup tasks are performed prior to that by renaming the exit function to some other name, like this:

rename exit __exit

Then we can define our own exit function, as shown in Listing 14.

Listing 14: Quitting the application

exit
proc exit {} {
   quitApp
}

The TickLeez function quitApp is shown in Listing 15. After we've inspected all movie windows and made sure that all changes have been saved, we remove the temporary directory we created earlier. Then we call __exit to let Tcl perform its cleanup tasks.

Listing 15: Handling the Quit menu item

quitApp
proc quitApp {} {
   global appData
   set cancelled 0
   
   while {([topMovieWindow] != "") && ($cancelled == 0)} {
      set cancelled [expr ![attemptClose [topMovieWindow] \
               quitting]]
   }
   if {$cancelled == 0} {
      # remove the temp directory if it exists
      if {[file exists $appData(tempDir)]} {
         file delete -force $appData(tempDir)
      }
      __exit
   }
}

Conclusion

This brings us to the end of our investigation of Tcl/Tk and QuickTimeTcl as a delivery vehicle for QuickTime applications. In just under four hundred lines of script (not counting the blank lines and comments in the file TickLeez.tcl), we've managed to construct a multi-window QuickTime playback and editing application that runs on both Macintosh and Windows computers. TickLeez exhibits all the standard user-interaction and document-related behaviors that we've come to expect of a QuickTime application, and it does so with a minimum of platform-specific code. I think that QuickTimeTcl provides a very nice wrapper for the underlying QuickTime APIs. Better still, its source code is freely available and can be easily modified to add capabilities that it does not currently provide. Neither Tcl/Tk nor QuickTimeTcl is perfect, but all three packages are under active development and promise to get even better as time goes by.

Acknowledgements

Thanks are due once again to Jim Ingham and to Mats Bengtsson for reviewing a draft of this article and for providing invaluable feedback.


Tim Monroe is a member of the QuickTime engineering team at Apple. You can contact him at monroe@mactech.com. The views expressed here are not necessarily shared by his employer.

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

Latest Forum Discussions

See All

Tokkun Studio unveils alpha trailer for...
We are back on the MMORPG news train, and this time it comes from the sort of international developers Tokkun Studio. They are based in France and Japan, so it counts. Anyway, semantics aside, they have released an alpha trailer for the upcoming... | Read more »
Win a host of exclusive in-game Honor of...
To celebrate its latest Jujutsu Kaisen crossover event, Honor of Kings is offering a bounty of login and achievement rewards kicking off the holiday season early. [Read more] | Read more »
Miraibo GO comes out swinging hard as it...
Having just launched what feels like yesterday, Dreamcube Studio is wasting no time adding events to their open-world survival Miraibo GO. Abyssal Souls arrives relatively in time for the spooky season and brings with it horrifying new partners to... | Read more »
Ditch the heavy binders and high price t...
As fun as the real-world equivalent and the very old Game Boy version are, the Pokemon Trading Card games have historically been received poorly on mobile. It is a very strange and confusing trend, but one that The Pokemon Company is determined to... | Read more »
Peace amongst mobile gamers is now shatt...
Some of the crazy folk tales from gaming have undoubtedly come from the EVE universe. Stories of spying, betrayal, and epic battles have entered history, and now the franchise expands as CCP Games launches EVE Galaxy Conquest, a free-to-play 4x... | Read more »
Lord of Nazarick, the turn-based RPG bas...
Crunchyroll and A PLUS JAPAN have just confirmed that Lord of Nazarick, their turn-based RPG based on the popular OVERLORD anime, is now available for iOS and Android. Starting today at 2PM CET, fans can download the game from Google Play and the... | Read more »
Digital Extremes' recent Devstream...
If you are anything like me you are impatiently waiting for Warframe: 1999 whilst simultaneously cursing the fact Excalibur Prime is permanently Vault locked. To keep us fed during our wait, Digital Extremes hosted a Double Devstream to dish out a... | Read more »
The Frozen Canvas adds a splash of colou...
It is time to grab your gloves and layer up, as Torchlight: Infinite is diving into the frozen tundra in its sixth season. The Frozen Canvas is a colourful new update that brings a stylish flair to the Netherrealm and puts creativity in the... | Read more »
Back When AOL WAS the Internet – The Tou...
In Episode 606 of The TouchArcade Show we kick things off talking about my plans for this weekend, which has resulted in this week’s show being a bit shorter than normal. We also go over some more updates on our Patreon situation, which has been... | Read more »
Creative Assembly's latest mobile p...
The Total War series has been slowly trickling onto mobile, which is a fantastic thing because most, if not all, of them are incredibly great fun. Creative Assembly's latest to get the Feral Interactive treatment into portable form is Total War:... | Read more »

Price Scanner via MacPrices.net

Early Black Friday Deal: Apple’s newly upgrad...
Amazon has Apple 13″ MacBook Airs with M2 CPUs and 16GB of RAM on early Black Friday sale for $200 off MSRP, only $799. Their prices are the lowest currently available for these newly upgraded 13″ M2... Read more
13-inch 8GB M2 MacBook Airs for $749, $250 of...
Best Buy has Apple 13″ MacBook Airs with M2 CPUs and 8GB of RAM in stock and on sale on their online store for $250 off MSRP. Prices start at $749. Their prices are the lowest currently available for... Read more
Amazon is offering an early Black Friday $100...
Amazon is offering early Black Friday discounts on Apple’s new 2024 WiFi iPad minis ranging up to $100 off MSRP, each with free shipping. These are the lowest prices available for new minis anywhere... Read more
Price Drop! Clearance 14-inch M3 MacBook Pros...
Best Buy is offering a $500 discount on clearance 14″ M3 MacBook Pros on their online store this week with prices available starting at only $1099. Prices valid for online orders only, in-store... Read more
Apple AirPods Pro with USB-C on early Black F...
A couple of Apple retailers are offering $70 (28%) discounts on Apple’s AirPods Pro with USB-C (and hearing aid capabilities) this weekend. These are early AirPods Black Friday discounts if you’re... Read more
Price drop! 13-inch M3 MacBook Airs now avail...
With yesterday’s across-the-board MacBook Air upgrade to 16GB of RAM standard, Apple has dropped prices on clearance 13″ 8GB M3 MacBook Airs, Certified Refurbished, to a new low starting at only $829... Read more
Price drop! Apple 15-inch M3 MacBook Airs now...
With yesterday’s release of 15-inch M3 MacBook Airs with 16GB of RAM standard, Apple has dropped prices on clearance Certified Refurbished 15″ 8GB M3 MacBook Airs to a new low starting at only $999.... Read more
Apple has clearance 15-inch M2 MacBook Airs a...
Apple has clearance, Certified Refurbished, 15″ M2 MacBook Airs now available starting at $929 and ranging up to $410 off original MSRP. These are the cheapest 15″ MacBook Airs for sale today at... Read more
Apple drops prices on 13-inch M2 MacBook Airs...
Apple has dropped prices on 13″ M2 MacBook Airs to a new low of only $749 in their Certified Refurbished store. These are the cheapest M2-powered MacBooks for sale at Apple. Apple’s one-year warranty... Read more
Clearance 13-inch M1 MacBook Airs available a...
Apple has clearance 13″ M1 MacBook Airs, Certified Refurbished, now available for $679 for 8-Core CPU/7-Core GPU/256GB models. Apple’s one-year warranty is included, shipping is free, and each... Read more

Jobs Board

Seasonal Cashier - *Apple* Blossom Mall - J...
Seasonal Cashier - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Read more
Seasonal Fine Jewelry Commission Associate -...
…Fine Jewelry Commission Associate - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) Read more
Seasonal Operations Associate - *Apple* Blo...
Seasonal Operations Associate - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Read more
Hair Stylist - *Apple* Blossom Mall - JCPen...
Hair Stylist - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Blossom Read more
Cashier - *Apple* Blossom Mall - JCPenney (...
Cashier - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Blossom Mall Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.